Skip to main content
Glama
zackunseasoned

mcp-militarypay

mcp-militarypay

An MCP server for published United States military pay rates: basic pay and Basic Allowance for Subsistence (BAS) from DFAS, and Basic Allowance for Housing (BAH) from the DoD Defense Travel Management Office (DTMO).

Unofficial. This reads public rate tables. It does not read anyone's Leave and Earnings Statement and it is not an authoritative source of pay. For actual pay questions contact DFAS at 1-888-332-7411 or use myPay.

DFAS publishes no developer API. All source data is public HTML tables and downloadable bulk files — no API key, no auth, no login.

How it works

Ingest and serve are separate. Parsing happens once at refresh time and writes to SQLite; every MCP tool call afterwards is a parameterized SELECT with no HTML or CSV parsing in the request path.

DFAS HTML  ─┐
DTMO ASCII ─┼─→  ingest (parse, validate, log)  ─→  SQLite  ─→  MCP tools
DFAS BAS   ─┘

At ~7k rows SQLite isn't chosen for speed — it's chosen because the ZIP → MHA → rate join is real SQL rather than chained dicts, multi-year history is painless, and you can open the file in any SQLite browser to eyeball a scrape against the live page.

Related MCP server: WageAPI

Install

python -m venv .venv
.venv/bin/pip install -e ".[dev]"          # Windows: .venv\Scripts\pip

Build the database

python -m mcp_militarypay.cli ingest --all --year 2026
python -m mcp_militarypay.cli verify       # check against known published values
python -m mcp_militarypay.cli status       # what's loaded, and when it was fetched
python -m mcp_militarypay.cli probe        # diagnose HTTP 403s (see below)
python -m mcp_militarypay.cli notes        # footnotes + flat rates captured
python -m mcp_militarypay.cli lookup --grade E-5 --years 6 --zip 92101 --dependents

lookup is an ad-hoc query for spot-checking against the published DFAS and DTMO lookups. It calls the same functions the MCP tools call, so a spot-check exercises the real path rather than a parallel one.

notes prints the footnote text captured from the DFAS pages and the flat rates read out of it, flagging any rate that could not be extracted. Those patterns are the most fragile part of the ingest, so this is the quickest way to check them against the live page.

The database defaults to data/militarypay.sqlite3 inside the checkout, or ~/.mcp-militarypay/ when installed as a normal (non-editable) package. Override with --db or the MILITARYPAY_DB environment variable.

An explicit --year that disagrees with the page's own "Effective January 1, YYYY" stamp is refused rather than stored, since filing one year's rates under another is precisely the silent staleness this is meant to avoid. Pass --allow-year-mismatch when that is deliberate.

Off-cycle BAH rate sets

BAH is not immutable per calendar year. Individual housing areas get mid-year adjustments — for example the 2026 temporary increase for Abilene, TX / Dyess AFB (MHA TX270), effective 2026-05-16. A design that assumes one file per year silently serves stale rates for that MHA.

Rate sets are therefore modelled by effective_date, and an off-cycle set is ingested as a distinct, partial set:

python -m mcp_militarypay.cli ingest \
    --bah-offcycle 2026-abilene-temp \
    --effective-date 2026-05-16 \
    --label "2026 Abilene Temporary Increase" \
    --year 2026 \
    --from-file "2026_BAH_Rates__Updated_with_TX270_Temporary_Increase.xlsx" \
    --baseline-file "2026_BAH_Rates.xlsx"

Both workbooks come from the BAH rate lookup page. --baseline-file does two jobs.

First, it derives the affected areas by diffing the two workbooks rather than trusting the filename: an off-cycle publication is the full annual table with a handful of areas changed, so ingesting all of it would duplicate 337 unchanged MHAs and blur which rates actually moved. (For the 2026 TX270 increase the diff returns exactly TX270.) Use --mha instead to name the areas explicitly.

Second, it restores the pre-change rates into the annual set for those areas. This matters more than it sounds: DTMO republishes the annual ASCII bundle in place when a mid-year adjustment lands, so once TX270 rises in May the January bundle no longer exists anywhere — a freshly downloaded BAH-ASCII-2026.zip already carries the increased figure under an effective date of 1 January. Without the restore, as_of=2026-03-01 returns the May rate for a member who was actually drawing the January one, which is precisely the back-pay and rate-protection question as_of exists to answer. The baseline workbook is the only surviving record of the original rates. Only rows that already exist are updated, only where the value differs, and the ingest reports how many were corrected. --no-restore-annual opts out.

Lookups then pick the most recent rate set covering that MHA, while every other MHA keeps the annual rate. Pass as_of to a lookup to get the rate in effect on a given date. Prior years are never overwritten on refresh — BAH individual rate protection means a member with uninterrupted eligibility doesn't take a decrease when published rates drop, so historical rates must stay queryable.

Troubleshooting: HTTP 403 from DFAS / DTMO

Both dfas.mil and travel.dod.mil sit behind a WAF that rejects clients which don't look like a browser. A custom User-Agent gets an outright HTTP 403 on every URL, on both hosts — which is what the first live run of this project hit. These are public rate tables with no authentication, no login and no API key, so the fix is simply to send the ordinary header set a browser sends, and that is now the default (a current Chrome UA, the usual Accept / Sec-Fetch-* / sec-ch-ua headers, over HTTP/2).

If a future WAF change breaks it again, don't guess one profile at a time:

python -m mcp_militarypay.cli probe

This tries four header profiles against each host and prints which ones get a 200, along with any WAF markers in the rejection body:

Profile

What it isolates

project-ua

the original custom User-Agent (the one that 403'd)

httpx-default

no custom headers at all

browser

full browser header set over HTTP/1.1

browser-http2

full browser header set over HTTP/2 — the current default

Then override the agent if a different one is needed:

export MILITARYPAY_USER_AGENT="..."     # Windows: $env:MILITARYPAY_USER_AGENT

probe distinguishes the two failure modes that look alike from the outside: an HTTP status code means the server answered and rejected the request, while a transport or proxy error means something between you and the server blocked it (a corporate/ISP filter or an egress policy) and no header change will help.

Run the server

python -m mcp_militarypay.server

The suite includes an integration test that launches this as a subprocess and speaks MCP to it over stdio, so the entry point, negotiation and error handling are covered on the same path a real client uses — including on Windows in CI.

The simplest route, and the one that avoids config-file trouble entirely:

python packaging/build_mcpb.py      # -> dist/militarypay-<version>.mcpb

Then in Claude: Settings → Extensions → Install Extension, pick the .mcpb, and point it at your militarypay.sqlite3 when it asks.

This matters on the Microsoft Store build of Claude, which runs in an MSIX container with a virtualised %APPDATA% — a hand-edited claude_desktop_config.json under %APPDATA%\Claude\ may not be the file the app actually reads, and the server then never loads with nothing obvious to show for it. Installing a bundle goes through the app's own flow instead.

Build it with the interpreter you want it to run under: dependencies are vendored from the running environment, so build on Windows to get Windows wheels. The bundle carries only what the server needs — it serves from an already-built database and never fetches, so the ingest's dependencies are left out. It needs no virtualenv at run time.

The manifest pins an absolute interpreter path. A host may launch python3 rather than the python a manifest asks for, and on a machine with several Pythons installed that can be a different version than the vendored wheels were built against — pydantic_core then fails to import its compiled extension, or python3 isn't on PATH at all and the spawn fails outright. The build pins the interpreter matching the vendored ABI (sys.base_prefix, not the virtualenv, so the bundle doesn't depend on the checkout staying put) and refuses to pack if that interpreter can't import them. --command python restores host resolution if you want a portable bundle and accept the risk.

Registering by hand

Register it with an MCP client (stdio transport):

{
  "mcpServers": {
    "militarypay": {
      "command": "C:\\path\\to\\mcp-militarypay\\.venv\\Scripts\\python.exe",
      "args": ["-m", "mcp_militarypay.server"],
      "env": { "MILITARYPAY_DB": "C:\\path\\to\\mcp-militarypay\\data\\militarypay.sqlite3" }
    }
  }
}

command must be the interpreter from the virtual environment the package was installed into, not a system Python. A system interpreter cannot import mcp_militarypay and the server exits with ModuleNotFoundError before it speaks any MCP, which a client reports only as a server that failed to start. Check it before restarting the client:

/path/to/.venv/bin/python -c "import mcp_militarypay; print('ok')"

The virtual environment also installs a console script, which avoids the question entirely — use it as command with no args:

.venv/bin/militarypay-mcp          # Windows: .venv\Scripts\militarypay-mcp.exe

Tools

All five are read-only (readOnlyHint, idempotentHint, openWorldHint: false — they query the local database, not the live web). Every response carries the effective date and source URL of the figures used.

Tool

Returns

get_base_pay(pay_grade, years_of_service, year?, months_active_duty?, senior_enlisted_advisor?)

Monthly basic pay (taxable) plus applicable footnotes

get_bah(zip_code, pay_grade, has_dependents, year?, as_of?)

Monthly BAH (non-taxable), resolved MHA code, rate set and effective date

find_housing_area(query, year?, limit?)

The Military Housing Area for a place, MHA code or ZIP, with ZIP codes to query it with

get_bas(pay_grade_type, year?, bas_ii?)

Monthly BAS (non-taxable)

estimate_total_compensation(pay_grade, years_of_service, zip_code, has_dependents, ...)

Base pay + BAH + BAS with a per-component breakdown and taxable/non-taxable split

get_database_status()

What data is loaded and when each source was last fetched

Entitlement rules that are actually implemented

These footnotes are the difference between a toy and a number someone can act on:

  • E-1 under 4 months of active duty is a different, lower rate than the E-1 table value. Pass months_active_duty.

  • Senior enlisted advisor billets (SEAC, SMA, MCPON, CMSAF, SMMC, CMSSF, MCPOCG, SEA to CNGB) are a flat rate regardless of years of service. Pass senior_enlisted_advisor=True.

  • Blank cells are not $0. E-8 has no published rate below "Over 8", E-9 none below "Over 10". Those combinations return a null rate and an explicit "not a valid combination" explanation.

  • Years-of-service banding is a range lookup, not a column-label match: 5 years of service is paid at the "Over 4" band.

  • Service academy cadets / midshipmen and ROTC members are a flat rate that is not on the officer grid at all.

  • BAS II is never a default. It's a conditional rate (2× standard enlisted BAS) requiring Service Secretary authorization; returned only when asked for.

  • BAH follows the permanent duty station, not the member's residence. A member assigned to Travis AFB but living in Winters draws the Travis rate, even though the home ZIP resolves to a Sacramento-area housing area and a lower figure. Every BAH response says so, and the zip_code parameter description says it where a caller reads it.

  • BAH rate protection is noted on every BAH response.

  • Housing areas are looked up, not guessed. BAH is published per Military Housing Area but get_bah takes a ZIP, so a caller with only a place name would otherwise supply a ZIP from memory — and a wrong one resolves to another real area and returns a confident rate for the wrong locality. find_housing_area searches by locality name, MHA code or ZIP and returns ZIP codes to pass on. DTMO names areas for localities, so an installation matches only where the published name includes it (Travis AFB finds VALLEJO/TRAVIS AFB, CA; Redstone Arsenal is inside HUNTSVILLE, AL) — the tool description says so, so a miss is not read as "no such place".

Data sources

Source

URL

Cadence

Basic pay (4 category pages)

dfas.mil/.../Pay-Tables/Basic-Pay/{EM,CO,CO_FE,WO}/

Annual, effective 1 Jan

BAS

dfas.mil/.../Pay-Tables/bas/

Annual, effective 1 Jan

BAH (ASCII bulk)

travel.dod.mil/Portals/119/.../ASCII/BAH-ASCII-{year}.zip

Annual plus off-cycle

The BAH ASCII bundle format

DTMO does publish a schema, but it is shipped inside the bundle itself as ASCII-FILE-FORMAT.pdf rather than on the website. It confirms the delimiters, the CHAR(5) MHA key, and the grade order — including the counterintuitive part, that O1E/O2E/O3E come before O1.

That PDF's field list stops at O7 (25 fields) where the published files carry 28, and since the list also runs off the bottom of its single page it looks truncated. It isn't. DTMO's Excel workbook independently publishes exactly 24 rate columns ending at O07, so O-7 really is the last distinct grade — the ASCII files pad three further columns repeating the O-7 value, which is DTMO's "O-7/O-7+" bucket. Reading that tail as O-8/O-9/O-10 gives correct rates either way, and verify checks those four columns agree within every MHA.

The Excel workbook

The other bulk download, and the only published form the off-cycle adjustments appear in — there is no separate ASCII bundle for, say, the 2026 TX270 temporary increase. It is a clean grid rather than the government-Excel hazard the design anticipated: two sheets (With / Without), a title row, a header row, one row per MHA.

MHA | MHA_NAME | E01..E09 | W01..W05 | O01E O02E O03E | O01..O07

It carries no ZIP-to-MHA crosswalk, so a set ingested from it is not an annual baseline; ZIP resolution goes through the annual set that has one.

BAH-ASCII-<year>.zip carries thirteen members; four are used:

File

Format

sorted_zipmha<yy>.txt

Space-delimited ZIP MHA (~41k US ZIPs)

bahw<yy>.txt

Rates with dependents

bahwo<yy>.txt

Rates without dependents

mhanames<yy>.txt

MHA code → locality name

The rest are .dat encodings of the same data, DTMO's own ASCII-FILE-FORMAT.pdf, and — importantly — the previous publication under "* - old.txt" / "* - old.dat" names. Those superseded files end in .txt and share the bahw/bahwo prefixes, so a filename-prefix fallback can silently ingest last publication's rates. They are excluded explicitly and tested for.

The rate files are headerless CSV — not fixed-width — with 28 fields:

MHA, E-1..E-9, W-1..W-5, O-1E..O-3E, O-1..O-10
 0    1  ..  9   10 .. 14   15 .. 17   18 .. 27

Note the BAH grade set is not the basic pay grade set. The DTMO lookup form collapses O-7 and above into a single "O-7/O-7+" bucket; in the ASCII files O-7..O-10 simply carry the same value.

Defensive parsing

The DFAS pages get reformatted (a 2026 page whose sidebar still says "2022 Active Duty Pay Days"), so page structure is treated as unstable:

  • Pay grades are regexed out of cell text, which carries footnote markers like E-9 (Notes 2 & 3) — never matched on exact cell text.

  • The basic pay grid is split across two HTML tables per page; both are read and joined on pay grade.

  • Columns are matched by header label, not position.

  • A page missing expected pay grades produces a warning; an unrecognizable page raises rather than writing a half-empty table.

  • A BAH row with the wrong field count fails loudly rather than mapping rates onto the wrong pay grades.

  • A page with no effective date is refused rather than filed under a guessed year.

  • Raw BAH lines are kept verbatim in raw_bah_lines. Diffing this year's raw rows against last year's is how a silent layout change gets caught.

  • Every ingest records a row count in source_fetch_log.

Tests

.venv/bin/python -m pytest

272 tests, no network required — the parsers run against fixtures in tests/fixtures/. Those fixtures are synthetic: they reproduce the documented structure of each source, and only the following figures are real published values, used as the assertions:

Value

2026

Basic pay, E-5 over 4

$3,946.80

BAS, enlisted

$476.95

BAS, officer

$328.48

BAS II

$953.90

E-1 under 4 months

$2,225.70

Senior enlisted advisor flat rate

$11,166.90

Everything else in the fixtures is obviously non-real filler. The repository ships no rate data — the database is built by the ingest, so the only rates ever served are ones fetched from DFAS/DTMO at refresh time.

Out of scope

  • myPay / LES / individual pay data — behind authentication, and it's personal financial data. Not a scraping target.

  • Special & incentive pays (flight, sea, sub, hazardous duty, health professions bonuses) — same clean-table format on the DFAS index, good phase 2.

  • Drill pay (reserve/guard) — four more tables, same structure as basic pay.

  • OHA / OCONUS COLA — different DTMO datasets, updated more often than annually.

  • Retirement calculators — rule-heavy and system-dependent (High-3, BRS).

Reference

Available Tools

6 tools
estimate_total_compensationEstimate Total CompensationA
Read-onlyIdempotent

Basic pay + BAH + BAS with a per-component breakdown and a tax split.

Returns monthly and annual totals separated into taxable (basic pay) and non-taxable (BAH, BAS) amounts. The ZIP code must be the permanent duty station, not the member's residence - BAH follows the duty station. This is regular military compensation only: it excludes special and incentive pays, bonuses, and all deductions.

ParametersJSON Schema
NameRequiredDescriptionDefault
yearNoYear. Defaults to the most recent loaded.
zip_codeYes5-digit ZIP code of the member's PERMANENT DUTY STATION, not their home. BAH follows the duty station, so a residence ZIP returns a different, real, wrong housing area.
pay_gradeYesPay grade, e.g. 'E-5', 'O-3'.
has_dependentsYesWhether the member has dependents.
years_of_serviceYesCumulative years of service.
months_active_dutyNoTotal months of active duty. Only affects E-1.
senior_enlisted_advisorNoTrue for a senior enlisted advisor billet.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already mark the tool as read-only and idempotent. The description adds valuable behavioral context: it returns a per-component breakdown with a taxable/non-taxable split, applies the duty-station rule for BAH, and scopes what is and is not included. No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with a compact summary of what the tool does, followed by output details, a key usage caveat, and scope exclusions. Each sentence earns its place, though the ZIP-code guidance is somewhat redundant with the schema description.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity, rich input schema, clear annotations, and existing output schema, the description is complete. It explains the calculation scope, correct ZIP semantics, and exclusions, leaving no critical ambiguity for an agent to call it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all seven parameters. The description reinforces the critical ZIP-code semantics (permanent duty station, not residence), but this is also fully stated in the schema. It does not add meaningfully new parameter-level information beyond what the schema provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific action (estimate total compensation), identifies the exact components (Basic pay + BAH + BAS), and describes the output shape (per-component breakdown, tax split, monthly/annual totals). This clearly distinguishes it from sibling tools like get_base_pay, get_bah, get_bas, and get_bas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear usage context: the ZIP code must be the permanent duty station, and the tool covers regular military compensation only, explicitly excluding special/incentive pays, bonuses, and deductions. However, it does not explicitly name sibling tools as alternatives for single-component or excluded-pay scenarios.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

find_housing_areaFind Housing AreaA
Read-onlyIdempotent

Find the Military Housing Area for a place, and ZIP codes to query it with.

Search for the duty station, not the member's home: BAH follows the permanent duty station, so a residence in a different housing area does not change the rate.

Use this before get_bah whenever you have a place name rather than a ZIP code, instead of supplying a ZIP from memory: a wrong ZIP resolves to some other real housing area and returns a confident rate for the wrong locality. Each result carries example ZIP codes to pass to get_bah.

Areas are named for localities, so a military installation matches only where the published name happens to include it - "Travis AFB" finds "VALLEJO/TRAVIS AFB, CA", but Redstone Arsenal is inside "HUNTSVILLE, AL". If a base name finds nothing, search the nearest city instead.

ParametersJSON Schema
NameRequiredDescriptionDefault
yearNoBAH year. Defaults to the most recent loaded.
limitNoMaximum areas to return.
queryYesA locality name ('San Diego'), an MHA code ('CA038'), or a ZIP code.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Even though annotations already mark the tool as read-only and idempotent, the description adds substantial behavioral context: BAH follows the permanent duty station, a wrong ZIP can resolve to a different real housing area, and locality-based naming means installations may not match directly. These are non-obvious behaviors that affect how an agent interprets results.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose, then builds context in compact paragraphs. Every sentence adds practical value, covering when to use it, why wrong ZIPs are dangerous, and how base names can fail to match. It is relatively long but each element is necessary for correct use.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a lookup tool with subtle locality-matching hazards, the description covers the key contextual points: relationship to get_bah, duty-station rule, ZIP risk, naming caveats, and a fallback search strategy. Combined with 100% schema coverage, rich annotations, and an output schema, nothing essential is missing for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema fully documents query, year, and limit. The description reinforces the role of query in the BAH workflow and mentions ZIP codes, but it does not add meaningfully new parameter-level semantics beyond what the schema already provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence names a specific verb and resource: 'Find the Military Housing Area for a place, and ZIP codes to query it with.' It clearly distinguishes the tool from get_bah, which is a sibling, by positioning this as the lookup step before rate retrieval.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly says 'Use this before get_bah whenever you have a place name rather than a ZIP code, instead of supplying a ZIP from memory,' and it explains the risk of a wrong ZIP. It also gives a concrete fallback strategy for base names that don't match published area names, making usage conditions actionable.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_bahGet BahA
Read-onlyIdempotent

Monthly Basic Allowance for Housing for a ZIP code and pay grade.

BAH is a non-taxable allowance. Resolves the ZIP to its Military Housing Area and returns the rate together with the MHA code, the rate set used and its effective date.

The ZIP must be the member's permanent duty station, not where they live. Someone stationed at one installation but living in another housing area still draws the duty station's rate, and a residence ZIP resolves to a different real area and returns a plausible but wrong figure. Where a housing area has an off-cycle adjustment, the most recent applicable rate set is used.

ParametersJSON Schema
NameRequiredDescriptionDefault
yearNoBAH year. Defaults to the most recent loaded.
as_ofNoISO date (YYYY-MM-DD). Returns the rate set in effect on that date, ignoring later off-cycle adjustments.
zip_codeYes5-digit ZIP code of the member's PERMANENT DUTY STATION, not their home. BAH follows the duty station, so a residence ZIP returns a different, real, wrong housing area.
pay_gradeYesPay grade, e.g. 'E-5', 'O-3', 'O-2E'.
has_dependentsYesWhether the member has dependents.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already mark the tool read-only and idempotent, and the description adds substantial behavioral context beyond that: it resolves the ZIP to a Military Housing Area, returns the rate set used and effective date, and selects the most recent applicable rate set for off-cycle adjustments. No contradiction with the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is organized into a clear summary followed by important caveats, and the duty-station warning is emphasized effectively. It is slightly repetitive in the residence-ZIP warning, but every sentence earns its place for such a nuanced lookup.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity, the description covers the input gotchas, the output components, and rate-set selection behavior. The presence of a rich input schema and output schema reduces the burden, and the description fills the remaining gaps well.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and each parameter already has a solid description, so the bar is a 3 baseline. The description adds value on top by explaining how the ZIP is resolved, why duty station matters, and how off-cycle adjustments affect year/as_of selections, which strengthens parameter understanding.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: it returns the Monthly Basic Allowance for Housing for a ZIP code and pay grade. It further distinguishes the tool from siblings like get_base_pay and get_bas by naming BAH and the exact return contents (MHA code, rate set, effective date).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives clear usage context, especially the crucial rule that the ZIP must be the permanent duty station, not residence, and explains the consequence of getting it wrong. It does not explicitly name sibling alternatives or state when not to use this tool, so it stops short of a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_basGet BasA
Read-onlyIdempotent

Monthly Basic Allowance for Subsistence for an officer or enlisted member.

BAS is a non-taxable allowance. Warrant officers receive the officer rate. BAS II is a conditional rate (twice standard enlisted BAS) requiring Service Secretary authorization - it is never the default answer.

ParametersJSON Schema
NameRequiredDescriptionDefault
yearNoYear. Defaults to the most recent loaded.
bas_iiNoReturn the conditional BAS II rate instead. Only when explicitly asked for.
pay_grade_typeYes'officer' (including warrant officers) or 'enlisted'.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A3.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint and idempotentHint, and the description adds meaningful behavioral context: BAS is non-taxable, warrant officers receive the officer rate, and BAS II is a conditional rate requiring Service Secretary authorization and is never the default. This goes beyond the structured fields without contradicting them.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three tight sentences with no fluff. The core definition is front-loaded, and the caveats about taxability and BAS II are placed where they are most useful. Every sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With full parameter documentation, an output schema, and safety annotations, the description covers the necessary lookup semantics well. The only notable gap is the lack of explicit guidance distinguishing this tool from sibling allowance tools, but the allowance-specific details make it sufficiently complete for the intended use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3. The description adds value by clarifying that 'officer' includes warrant officers for pay_grade_type and reinforcing that bas_ii is conditional and not the default, which strengthens the schema's existing parameter documentation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly identifies the tool's purpose: retrieving the Monthly Basic Allowance for Subsistence for an officer or enlisted member. It adds useful context about BAS being non-taxable and warrant officers receiving the officer rate, which helps differentiate BAS from sibling allowance tools, though it does not explicitly name alternatives.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no explicit guidance on when to use get_bas versus get_base_pay, get_bah, or estimate_total_compensation. The BAS II note is a parameter-level caveat rather than tool-selection guidance, so the agent is left to infer usage context from the tool name and description.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_base_payGet Base PayA
Read-onlyIdempotent

Monthly basic pay for a pay grade at a given years-of-service band.

Basic pay is taxable income. Returns the rate plus any footnotes that apply to that grade. A grade/years-of-service combination that does not exist on the table (for example E-8 at 2 years) is reported as an invalid combination with a null rate - not as zero.

ParametersJSON Schema
NameRequiredDescriptionDefault
yearNoPay table year. Defaults to the most recent loaded.
pay_gradeYesPay grade, e.g. 'E-5', 'O-3', 'W-2', 'O-1E'.
years_of_serviceYesCumulative years of service.
months_active_dutyNoTotal months of active duty. Only affects E-1: under 4 months is a different, lower rate.
senior_enlisted_advisorNoTrue for a senior enlisted advisor billet (SEAC, SMA, MCPON, CMSAF, SMMC, CMSSF, MCPOCG, SEA to CNGB), which is a flat rate regardless of service.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A3.9/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate read-only and idempotent behavior. The description adds valuable behavioral context beyond that: basic pay is taxable income, footnotes are included, and invalid grade/YOS combinations return a null rate rather than zero. This meaningfully shapes result interpretation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and front-loaded: the first sentence states the core purpose, and the next two add only high-value distinctions: taxable income and null-vs-zero behavior. There is no filler or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complete input schema and presence of an output schema, the description covers the non-obvious behavioral details an agent needs: what is returned, what null means, and the taxable nature of the pay. Nothing essential for selecting or invoking the tool is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema covers 100% of parameters with descriptions, so the description does not need to repeat them. It adds a cross-parameter behavioral detail about invalid combinations returning null, but no per-parameter information beyond what the schema already provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the resource: monthly basic pay for a pay grade and years-of-service band, and what is returned: the rate plus footnotes. It does not explicitly differentiate from sibling tools like get_bah or get_bas, though the subject matter makes the distinction reasonably clear.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies the intended use: look up monthly basic pay by grade and years of service. It does not provide explicit when-to-use versus when-not-to-use guidance or reference alternatives like estimate_total_compensation, so the agent must infer the boundary.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_database_statusGet Database StatusA
Read-onlyIdempotent

What rate data is loaded and when each source was last fetched.

Use this to check how current the figures are before relying on them, or to find out why a lookup came back empty.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already mark the tool as read-only and idempotent, so no safety concerns are hidden. The description adds useful behavioral context beyond annotations by explaining what the status reveals and how it aids in interpreting empty lookup results.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two short sentences with no filler. The first sentence delivers the core output, and the second sentence states practical usage, making it efficient and front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a parameterless status tool with an output schema and read-only annotations, the description is complete. It explains what the tool reports and when to use it, leaving no critical gaps for an agent to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, so the empty input schema fully documents the interface. The description does not need to clarify parameter meaning; the baseline of 4 applies due to the absence of parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states what the tool provides: loaded rate data and the last-fetch time for each source. It is unambiguously a status/metadata tool, distinct from the sibling tools that return pay, housing, and compensation values.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives two concrete situations to use it: checking data freshness before relying on figures, and diagnosing why a lookup returned empty. It does not explicitly name alternative tools or when not to use it, but the provided contexts are clear and actionable.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 6 tool updatesv0.1.0
    • First observedestimate_total_compensation
    • First observedfind_housing_area
    • First observedget_bah
    • First observedget_bas
    • First observedget_base_pay
    • First observedget_database_status

TDQS

A4.3/5.0

Scored across 6 tools

Disambiguation5/5

Each getter targets a distinct pay component (basic pay, BAH, BAS), find_housing_area is a supporting lookup for BAH, and estimate_total_compensation is clearly a combined calculation rather than a competing single-rate lookup. There is no realistic ambiguity between tool purposes.

Naming Consistency5/5

All tools use a clear snake_case verb_noun pattern: get_ for direct lookups, find_ for the area search, and estimate_ for the aggregate calculation. The naming style is consistent and each verb accurately reflects the action.

Tool Count5/5

Six tools is well-scoped for a read-only military compensation lookup service. Each tool has a distinct role, and there are no redundant or filler tools.

Completeness4/5

The server covers the regular military compensation triad (basic pay, BAH, BAS) along with housing-area resolution and data-status checks. It explicitly excludes special/incentive pays, bonuses, and deductions, which is a minor but real gap for users expecting full military pay coverage.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • -
    license
    Not graded
    quality
    Not graded
    maintenance
    Provides weather forecast data from the US National Weather Service API through MCP tools. Enables users to get detailed weather forecasts by ZIP code or coordinates using natural language queries.
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    US + EU salary benchmarking, pay transparency compliance, and semantic endpoints. 1,400+ US occupations, 28 EU countries. MCP server for AI agents.
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    MCP server that calculates work hours, overtime, and gross pay from clock in/out entries with support for federal, California, Alaska, Colorado, and Nevada overtime rules.
    2
    42 npm
    2
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    MCP server for HUD housing data, enabling Fair Market Rent lookups, Section 8 income limits, and ZIP-to-county crosswalk mapping for affordable housing assessments.
    6
    4 npm
    MIT