MatchAwards MCP server
OfficialClick on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@MatchAwards MCP serverFind open roofing contracts in Virginia posted in the last week."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
MatchAwards MCP server
MatchAwards gives AI agents direct access to continuously updated government and business contracts, grants, jobs, awards and collaboration opportunities.
This is the Model Context Protocol server for MatchAwards. It lets Claude, Cursor, VS Code and other MCP clients search US government contract opportunities, grants and jobs, and past contract and grant awards. It only reads data, and it needs no account and no API key.
Tools
Tool | What it returns | Example prompt |
| Federal and state contract opportunities. Filters: NAICS (up to 10 codes), state, keyword, set-aside, federal or state only, posted within N days, open only, agency (by name), 1 to 50 results, paging. | "Find open roofing contracts in Virginia posted in the last week." |
| Federal grant and funding opportunities. Filters: NAICS, keyword, posted within N days, open only, 1 to 50 results, paging (grant notices carry no state). | "Are there open broadband grants?" |
| Job postings from the last 30 days. Filters: NAICS, state, keyword, posted within N days, 1 to 50 results, paging. | "Show electrician jobs in Texas from the last 7 days." |
| The full record of one opportunity: the search row plus its description, published points of contact (name and title) and, where MatchAwards has them, AI-generated notes ( | "Who do I contact about the second result?" |
| NAICS codes from words ("cybersecurity") or from a 2 to 6 digit code prefix, with each code's page on matchawards.com. 1 to 25 codes. | "What NAICS code covers cybersecurity consulting?" |
| Past federal contract awards and grant awards: awardee (name, UEI, DUNS), amount, date, agency, NAICS, set-aside, place, solicitation, contract and CFDA numbers, links to matchawards.com and to SAM.gov or USAspending. Filters: type, NAICS, agency (by name), state, awardee name, UEI, keyword, awarded within N days (up to 1,095), 1 to 50 results, paging. | "Who won VA contracts under NAICS 541512 in the last year?" |
| One firm's award history by UEI: names, location (city, state, country), contract and grant totals, first and last award dates, top NAICS codes and agencies, the 10 most recent awards. No contact details. | "Show the award history of UEI C3KXNLTAAY98." |
| A qualification brief on one opportunity: days left (US Eastern), open or closed, set-aside and NAICS (and whether they match the user's | "Should we bid on the first result? We're NAICS 336413, small business." |
| Up to 10 firms that won similar awards, from the newest records read ( | "Who could we team with on that Air Force notice?" |
The agency filter takes a name. The server looks it up among MatchAwards' agencies (exact name first, then names that start with it, then the rest in the API's order) and filters by the best match only, for now; the result's note names that agency and lists up to three other matches (one department can have several agency accounts), and when nothing matches, the search covers all agencies and the note says so. Lookups are cached for 10 minutes.
Award records may be incomplete (federal awards especially), so every award result says so. The two brief tools describe firms as having won similar work, never as the incumbent; they make at most 1 opportunity request and 6 award requests per call (at most 3 per query, shared by the queries), within 6 seconds, and when either limit cuts the lookup short they answer with what they read and a warning. Their sample says how many award records the figures rest on, their dates, and whether every matching record was read. A grant award's title is its recipient's name, so the model is asked to head it with the awardee name and CFDA number instead.
Every tool is read-only and needs no account (except my_matches, a hosted tool that exists only with account linking on; see Linked accounts): each declares readOnlyHint, idempotentHint, a title and securitySchemes: [{"type": "noauth"}] (in _meta). Results are structured: each tool publishes an outputSchema and returns structuredContent, with the same JSON as text for clients that read only text. Every result carries a url on matchawards.com (and a sam_url for federal notices), and the server asks the model to show them as markdown links. Links to matchawards.com carry utm_source, utm_medium and utm_campaign parameters naming the AI host the request came from (for example utm_source=chatgpt); links to other sites are left unchanged.
Changes in 1.1.6
Rate limits (hosted): only tool calls count in the per-client limit, now 60 per minute and 20 per 5 seconds (the MatchAwards API's own per-address limit; was 30 and 10 for every request), 240 per minute per IPv6 /48. Handshake and listing requests, which coding agents such as Codex repeat before every call, have their own budget of 600 per minute and 120 per 5 seconds (
MATCHAWARDS_PROTOCOL_PER_MIN,MATCHAWARDS_PROTOCOL_BURST), with 6000 per minute for all clients together (MATCHAWARDS_PROTOCOL_GLOBAL_PER_MIN, the only one that applies to ChatGPT's addresses). Rejections count asrate_limit_protocolandrate_limit_protocol_global. A body that does not arrive within 10 seconds gets 408 (timeout).The Codex CLI (User-Agent
codex-mcp-client/...) is its own family,codex, in the usage metrics. Its links keeputm_source=mcp.
Changes in 1.1.5
Go-links (hosted,
MATCHAWARDS_GO_LINKS=1): an opportunity'ssam_urland an award'ssource_urlare given as the record'shttps://matchawards.com/go/<utm_source>/<id>link, which redirects to the same SAM.gov or USAspending page and counts the hand-off. A record without a MatchAwards id or page keeps its plain link. Off by default.
Changes in 1.1.4
search_contracts: the first page of a NAICS search that found something tells the model it may offer, once per conversation, a free MatchAwards account that sends new matches for those codes by email, with a sign-up link carrying the host's UTM parameters.my_matchesnever offers it.MATCHAWARDS_CHATGPT_SIGNIN=0(hosted, linking on): while ChatGPT's connection offers no sign-in,my_matchesanswers ChatGPT without the challenge (ChatGPT shows one only as an internal error): sign-in isn't available there yet, search by NAICS codes instead. Counted as outcomesignin_unavailable.
Changes in 1.1.3
my_matcheswithout a linked account: when the app gives the user no way to sign in, the model says plainly that signing in to MatchAwards isn't available there yet (not an error), asks what the business does, finds the codes withlookup_naicsand searches withsearch_contracts.
Changes in 1.1.2
No NAICS codes given: the instructions have the model ask what the user's business does (or its codes) and find the codes with
lookup_naics, in every mode. With linking on,my_matcheson a profile without codes asks the same, searches with the codes found (keeping the profile's state and set-aside) and offers the match-settings link to save them."Nothing here needs an account" is in the instructions only with linking off; with linking on it contradicted
my_matches.The keyword hint says keyword search matches whole words in titles, and to try another form of the word.
Changes in 1.1.1
Hosted server:
MATCHAWARDS_API_MAX_IN_FLIGHT(default 16) caps the requests to the public API in flight at once for the whole server. A request waits up to 5 seconds for a free slot (less when the tool's own time limit is nearer), then the tool answers "busy, retry" without calling the API. The per-minute API budget is spent only once a slot is held.
Changes in 1.1.0
Tool choice:
assess_opportunityis described as the tool for fit, bid/no-bid and bid-brief questions, andget_opportunitypoints those questions to it (contracts and grants only).Keyword hint: a contract, grant or award search by keyword with no NAICS code and only a few rows says in its note that keyword search matches titles only and points to
lookup_naics.Hosted server metrics: tool calls by User-Agent family, rows returned per list tool, MatchAwards API timing per endpoint and outcome, API requests per tool call, distinct ChatGPT conversations and first-seen ChatGPT users. Stats show the empty-result rate per tool; the ledger gains a
rowscolumn, added at open.Hosted server: optional account linking and the
my_matchestool (see Linked accounts). Off unlessMATCHAWARDS_AUTH_ISSUERis set; with it off nothing else changes.
Changes in 1.0.4
The result card renders in Claude: its
_meta.ui.domainis left out for Claude, which accepts only its own sandbox domain; other hosts still gethttps://matchawards.com.
Changes in 1.0.3
Hosted server: usage-stat rows are kept 180 days by default (was 365), set by
MATCHAWARDS_USAGE_RETENTION_DAYS.
Changes in 1.0.2
Cards come from a new tool,
show_results, which the model calls once with the ids it picked; the search tools return data only. The card (results-v2.html) expands a row in place and has an "Ask about this" button.Results filtered by
naicssay which requested codes matched:matched_naics(the record's own codes) and, on contract notices,ai_matched_naics(codes MatchAwards' AI suggested).Links to matchawards.com carry UTM parameters naming the AI host.
Hosted server: the shared limit is 1200 requests per minute, and
/metricsadds tool latency, distinct-caller and client counts.
Changes in 1.0
search_by_naicsis removed:search_contracts,search_grantsandsearch_jobstake the samenaicsfilter on its own.find_contactsis removed:get_opportunityreturns the published contacts with the rest of the record.Results are structured output (
outputSchemaplusstructuredContent). Fields the API adds later appear in results only once the schema declares them.get_opportunityreturnsai(AI-generated notes, labelled as such) where MatchAwards has them, else null.Tool annotations are closed-world (
openWorldHint: false): the tools read the MatchAwards catalog only.New tools:
lookup_naics,search_awards,get_company,assess_opportunityandfind_teaming_partners;search_contractstakes anagencyname.
Related MCP server: Government Contracts MCP
Cards
Hosts that render MCP Apps (ChatGPT, Claude) get one inline card per answer: after searching, the model calls show_results with up to 10 contract or grant ids it picked, and the card lists up to 8 of them with title, agency, place, a federal/state/grant badge, the deadline (days left, closed, or no fixed deadline), set-aside and NAICS code. Ids it could not show (not found, a job, or not fetched because MatchAwards was busy) are counted under the list. The search tools return data only, with no card. Clicking a row expands it (posted date, solicitation, office, NAICS, set-aside); its "Ask about this" button asks the model for that opportunity's details, and its "Open on MatchAwards" link opens its page. The card is one self-contained HTML resource, ui://matchawards/results-v2.html (text/html;profile=mcp-app): no scripts or styles from elsewhere and no network requests, so its CSP allows nothing. It renders only the tool's structuredContent, and never calls a tool itself.
The other tools return their structured results without a card.
Cards are off by default. Set MATCHAWARDS_WIDGETS=1 to list the resource and show_results, which points at it (_meta.ui.resourceUri); the other tools' results are the same either way. tests/widget_harness/ holds a minimal host page with sample results for checking the card in a browser.
Hosted
A hosted endpoint runs at https://matchawards.com/mcp. Add it as a remote MCP server (connector) by URL, with nothing to install and no key:
ChatGPT: add a custom connector with the URL
https://matchawards.com/mcp.Claude (claude.ai or Claude Desktop): Settings, Connectors, add a custom connector with the same URL. Claude Code:
claude mcp add --transport http matchawards https://matchawards.com/mcp.Cursor: in
mcp.json,{"mcpServers": {"matchawards": {"url": "https://matchawards.com/mcp"}}}.
The hosted endpoint serves the same tools. Tool calls are limited per client address (IPv6: per /64) to 60 per minute and 20 per 5 seconds, per IPv6 /48 to 240 per minute, and to 1200 per minute for all clients together (ChatGPT's shared pool: 600). Handshake and listing requests (initialize, tools/list, prompts/list, resources/list, resources/read, ping, notifications) have their own budget of 600 per minute and 120 per 5 seconds per client (per IPv6 /48: 2400 per minute; all clients together: 6000 per minute), so a client that reconnects before every call is not stopped by them. Over a limit it answers HTTP 429 with Retry-After.
Install
The server runs with uvx, which comes with uv.
Claude Desktop
Add this to claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/claude_desktop_config.json, Windows: %APPDATA%\Claude\claude_desktop_config.json), then restart Claude Desktop:
{
"mcpServers": {
"matchawards": {
"command": "uvx",
"args": ["matchawards-mcp"]
}
}
}Claude Code
claude mcp add matchawards -- uvx matchawards-mcpCursor
Add this to ~/.cursor/mcp.json (all projects) or .cursor/mcp.json (one project):
{
"mcpServers": {
"matchawards": {
"command": "uvx",
"args": ["matchawards-mcp"]
}
}
}VS Code
Add this to .vscode/mcp.json in your workspace:
{
"servers": {
"matchawards": {
"type": "stdio",
"command": "uvx",
"args": ["matchawards-mcp"]
}
}
}Other clients
Any MCP client that can start a stdio server works. The command is uvx matchawards-mcp.
OpenClaw
integrations/openclaw-plugin holds a native OpenClaw plugin (@matchawards/openclaw-plugin on ClawHub) whose nine tools call the hosted endpoint, and a ClawHub skill, matchawards, that teaches an agent the public REST API with curl. Both are read-only and need no account. The plugin's tools.json is generated from this server's tool list (integrations/openclaw-plugin/scripts/gen_tools.py), and the test suite fails when it is out of date.
Run the HTTP server yourself
matchawards-mcp --http serves the same tools over stateless Streamable HTTP (JSON replies, no sessions, no SSE) at http://127.0.0.1:8765/mcp:
uvx matchawards-mcp --http
curl -s http://127.0.0.1:8765/mcp -H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' -H 'MCP-Protocol-Version: 2025-06-18' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'How requests are handled, in order:
Repeated and trailing slashes are folded, so
/mcp/and//mcpcount as/mcp. Any path other than/mcpand/healthzgets 404 (with account linking on,/.well-known/oauth-protected-resourceand/.well-known/oauth-protected-resource/mcpserve the protected-resource metadata, like/healthzon any host and not rate-limited).GET /healthzreturnsokand is not rate-limited.A
Hostheader not inMATCHAWARDS_ALLOWED_HOSTS(or127.0.0.1,localhost) gets 421, whatever the method.Only
POST /mcpis served.GET,DELETEandOPTIONSget 405 (no SSE stream, no sessions, no CORS).A POST needs
Content-Length(411 without it), and bodies over 64 KB get 413.Rate limit per client (per ChatGPT user from OpenAI's addresses), then the server-wide cap: 429 with
Retry-After. The body is read first (it must arrive within 10 seconds, else 408), to tell a handshake or listing request (initialize,ping,server/discover,tools/list,prompts/list,resources/list,resources/templates/list,resources/read, any notification) from the rest, by the method in its first 8 KB: those count only in the protocol budgets (per client:MATCHAWARDS_PROTOCOL_PER_MIN/_BURST, per IPv6 /48 four times the per-minute one; all together:MATCHAWARDS_PROTOCOL_GLOBAL_PER_MIN), and from OpenAI's addresses only in the all-together one. Anything else (a tool call, another or unknown method, a batch, a body that is not JSON or longer than 8 KB) counts in the tool-call limits.With account linking on, a
tools/callof a tool that needs a linked account, without a usable token, gets 401 (403 when the token lacks the scope) withWWW-Authenticate; ChatGPT gets the same challenge as a tool result. Nothing else is checked.
The client is the socket address, or the X-Real-IP header when the socket address is in MATCHAWARDS_TRUSTED_PROXIES (IPv6 is keyed per /64; all /64s inside one /48 also share MATCHAWARDS_RATE_PER_PREFIX_MIN). Run it behind a reverse proxy that sets X-Real-IP, and do not expose the port directly. The limits are kept in memory per process. A bad value in any of the variables below stops the server at startup. The log has one line per request (method, path, status, duration, a hashed client key), never headers or bodies.
ChatGPT calls come from OpenAI's shared egress addresses (published list, bundled with the package and refreshed daily; a failed refresh keeps the last good list), so one address carries many users and a per-IP limit would throttle them together. From those addresses only, a request is limited per x-openai-subject (ChatGPT's per-user id, kept only as a keyed hash) and all ChatGPT traffic shares MATCHAWARDS_EGRESS_PER_MIN; a request without the header counts in that pool only. The header from any other address is ignored, and the User-Agent is never used.
In Docker, nginx on the host reaches the container from the compose network's gateway, not from loopback, so name that address exactly. Read it with docker network inspect <network> --format '{{(index .IPAM.Config 0).Gateway}}' and set, for example:
MATCHAWARDS_HTTP_HOST=0.0.0.0
MATCHAWARDS_TRUSTED_PROXIES=127.0.0.1/32,::1/128,172.18.0.1/32Do not trust a broad private range such as 172.16.0.0/12: it may overlap your internal network, and any host in it could then pick its own rate-limit key.
Configuration
Variable | Default | Purpose |
|
| API base URL. Change it only for development. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| unset |
|
| unset |
|
|
|
|
|
|
|
|
|
|
| unset |
|
|
|
|
|
|
|
|
|
|
|
|
|
| unset |
|
Linked accounts
Hosted mode (--http) only, and off unless MATCHAWARDS_AUTH_ISSUER is set. Off, the tool list, the instructions and every result are the same as without it.
On, a user can link their MatchAwards account (sign in with Google or email on matchawards.com) and gets one more tool, my_matches: open contracts matching the NAICS codes and state on their MatchAwards profile, with the rows, paging and links of search_contracts (type narrows to federal or state, ignore_state drops the state filter, which skips federal notices without a state). Notices open to all firms are included; only_my_set_aside keeps only those set aside for the profile's set-aside (federal notices only: a state-only search never takes a set-aside), and otherwise the note names it so the model can point out the set-aside ones. profile_used says which filters came from the profile (the set-aside's code and name). The search takes the first 10 NAICS codes on the profile, and the note says so when it has more. A profile without NAICS codes gets no search, and a settings_url to its match settings instead; its note asks the model to ask the user what their business does, find the codes with lookup_naics, search with them and offer the link to save them. Every other tool still needs no account and never reads a token.
matchawards.com is the OAuth authorization server; this server is only the resource server, and serves its protected-resource metadata (RFC 9728) at
/.well-known/oauth-protected-resource/mcp. The only scope isprofile:read.my_matchesdeclaressecuritySchemes[{"type": "noauth"}, {"type": "oauth2", "scopes": ["profile:read"]}]; every other tool keepsnoauthonly.A
tools/callofmy_matcheswithout a usable token gets a challenge naming the metadata and the scope. Claude and other MCP clients get HTTP 401 (403 when the token lacksprofile:read) withWWW-Authenticate, before the tool runs, and start linking on it. ChatGPT (User-Agentopenai-mcp) links on a tool result instead, so it gets a 200 result withisErrorand the same header value in_meta["mcp/www_authenticate"]. When MatchAwards does not answer the token check, the call fails with a "busy, retry" error, and when it refuses the account itself (a disabled or suspended account, or a token of an app that is not an agent app) with a plain error: neither is a challenge, since linking again would not help.The token check is one
GET {MATCHAWARDS_AGENT_API_BASE}/api/agent/v1/mewith the token. Only a token issued for this server (MATCHAWARDS_RESOURCE, RFC 8707) is taken (scheme and host compared case-insensitively, the path exactly), and not one whosetoken_expires_athas passed. From the answer only the account's opaque id (sub), its NAICS codes, set-aside, state, UEI, organization name and match-settings link (only an https link on matchawards.com) are read. The answer is kept in memory for 60 seconds (a rejected token for 30), under a keyed hash of the token.What is kept: the ledger's
linkedflag on each tool call (whether it ran with a linked account), and in memory a salted hash of the account'ssubfor the distinct-account gauge. Tokens, theAuthorizationheader and account ids are never stored or logged.
Usage stats (hosted mode)
With --http the server keeps usage stats in a SQLite file (MATCHAWARDS_USAGE_DB, WAL mode). stdio mode records nothing. Writes run on two background threads and are best-effort: a failed write is counted in mcp_ledger_errors_total and never fails a request. Rows older than MATCHAWARDS_USAGE_RETENTION_DAYS days (default 180) are deleted, at most once a day.
What is recorded:
Tool calls: one row per call that reached a tool: time, tool name, outcome (
ok,invalid_input,upstream_error,bad_response(a reply the tools cannot read),upstream_timeout(the API ran out of time; retry),partial(assess_opportunityorfind_teaming_partnersanswered, but its request or time limit cut the award lookup short),rate_limited_upstream,busy,internal_error,link_required(my_matchesanswered with the linking challenge),forbidden(my_matchesfor an account MatchAwards refuses)), whether it ran with a linked account (linked, 0 or 1; never which account), the rows a list-returning tool answered with (rows:search_contracts,search_grants,search_jobs,search_awards,lookup_naics,find_teaming_partners(firms),show_results(ids shown) andmy_matches, on anokorpartialcall; NULL for other tools and other outcomes; added to an older ledger at open), the client network (IPv4 /24 or IPv6 /48), the User-Agent (first 200 characters), its family (openai-mcp,codex,claude-user,claude,cursor, ...,proberorother) and the latency. Calls stopped by the rate limit are counted in a metric, with no row. Arguments the SDK rejects before the tool runs leave no row either; they still count in the transport table astools/call.Transport, per day: one counter per client network, JSON-RPC method (known MCP methods only, anything else is
_other) andclientInfoname and version, with the last User-Agent seen.clientInfois read from the first 8 KiB of aninitializebody, or fromparams._meta(2026-07-28 clients), as the body streams past; the body is not buffered. At most 50 counters per client network and day; past that they fold into_other.ChatGPT callers: when
MATCHAWARDS_USAGE_SALTis set and a request from ChatGPT's egress addresses carriesx-openai-subject(ChatGPT's anonymous per-user id; from any other address it is ignored), the tool-call row stores a 16-hex HMAC-SHA256 of it, to count distinct ChatGPT callers (openai-mcpfamily only in the stats).x-openai-sessionchanges per conversation, so it is never written to the ledger; it only feeds the in-memorychatgpt_sessiongauge (see Metrics), under the same conditions asx-openai-subject.
Never recorded: tool arguments (search terms, ids), request or response bodies, full client addresses, raw header values, tokens and account ids.
Read the stats with:
matchawards-mcp stats --days 7 # --db PATH to read another fileIt prints tool calls per tool and outcome, calls per family, real calls vs probers vs our own tests (matchawards-test/ UA), distinct client networks (probers and own tests left out), distinct ChatGPT fingerprints, tool calls with a linked account, the empty-result rate per list-returning tool (rows = 0 over calls with rows set), transport requests per method and the top clientInfo names. In Docker: docker exec <container> matchawards-mcp stats --days 1.
Metrics
When MATCHAWARDS_METRICS_TOKEN is set, a second listener in the same process (MATCHAWARDS_METRICS_HOST:MATCHAWARDS_METRICS_PORT, default 127.0.0.1:9765) serves GET /metrics in the Prometheus text format, with Authorization: Bearer <token> (401 without it). The /mcp port never serves /metrics (404): it trusts X-Real-IP from the proxy, so publish only the metrics port to the network Prometheus scrapes from. All metrics are in memory and reset on restart; every label value comes from a fixed set, never a client address, subject or argument.
mcp_tool_calls_total{tool,outcome,family}(counter): tool calls (the ledger writes one row for each), by User-Agentfamily(the ledger's fixed set:openai-mcp,claude-user,claude,cursor, ...,proberorother; never the raw User-Agent).mcp_tool_duration_seconds{tool}(histogram, buckets 0.1 0.25 0.5 1 2.5 5 10 20 30 s): tool call latency, for the same calls.mcp_tool_result_rows{tool}(histogram, buckets 0 1 5 10 25 50): rows a list-returning tool answered with, once perokorpartialcall (the ledger'srows).mcp_tool_api_pages{tool}(histogram, buckets 0 1 2 3 5): MatchAwards API requests one tool call made: search pages (the cursor followed while fillinglimit), the agency-name lookup (not when cached), the detail and award lookups of the composite tools,show_results' fetches. 0 when the server-wide API budget answeredbusybefore any request.mcp_api_duration_seconds{endpoint,outcome}(histogram, the buckets ofmcp_tool_duration_seconds): latency of every MatchAwards API request, to tell API time from the rest of a tool call.endpoint:search(opportunity search),awards,opportunity(one record),company,naics,agencies,me(the account-linking profile check) orother;outcome:ok(an answer below HTTP 400),client_error(any other 4xx: a rejected parameter, an unknown id, a 401 or 403 from the profile check, a 429),error(5xx, or no connection), ortimeout(no answer within 10 s, the API's own timeout answer, or cut off by a tool's time limit). A request cancelled for any other reason (the client went away, shutdown) is not recorded.mcp_requests_total{method}(counter): requests served on/mcpby JSON-RPC method.mcp_rejected_total{reason}(counter):rate_limit_client,rate_limit_protocol,rate_limit_protocol_global,rate_limit_prefix,rate_limit_subject,rate_limit_egress,rate_limit_global,size,timeout,host,path,method.mcp_agent_requests_total{family}(counter): requests served by User-Agent family.mcp_clients_total{client}(counter):initializerequests byclientInfo.name, folded intochatgpt,claude,cursor,vscode,n8n,matchawards,mcp-inspector,otherorunknown.mcp_distinct_callers{kind,window}(gauge,window=1hor24h): distinctclientnetworks (/24 or /48, probers and own tests left out) distinct ChatGPT users (chatgpt_subject:x-openai-subjecton requests from ChatGPT's egress addresses only), distinct ChatGPT conversations (chatgpt_session:x-openai-session, accepted exactly asx-openai-subjectis; in memory only, never in the ledger) and distinct linked accounts (account, listed only with account linking on). Kept as per-process salted hashes, at most 100,000 per kind. The distinct-caller gauges reset on restart, so a 24h value is only complete 24 hours after a start.mcp_distinct_evictions_total{kind}(counter): distinct-caller keys dropped to stay under that cap (non-zero only under a flood of new networks or subjects). While it is above zero the distinct counts are a lower bound: a caller rotating subjects or networks can push others out.mcp_ledger_errors_total(counter): usage ledger writes that failed.mcp_chatgpt_subjects_new_total(counter): tool calls from a ChatGPT user whose fingerprint has no earlier row in the ledger (first seen within the retention window), checked by the ledger writer and counted once that write has committed. NeedsMATCHAWARDS_USAGE_SALT, like the fingerprints; best-effort like other ledger writes (a failed write counts inmcp_ledger_errors_total, never in this).mcp_link_challenges_total{host,error}(counter, listed only with account linking on): challenges sent, byhost(chatgpt,claude,other) anderror(invalid_token,insufficient_scope).mcp_linked_calls_total{tool}(counter, listed only with account linking on): tool calls that ran with a linked account.
Ops notes
Mount a volume at
/data(or setMATCHAWARDS_USAGE_DB) so the file survives a container rebuild.For ChatGPT fingerprints, nginx must pass
x-openai-subjectthrough to the container (it does by default unless headers are cleared), andMATCHAWARDS_USAGE_SALTmust be set and kept stable: a new salt starts new fingerprints.For Docker, set
MATCHAWARDS_METRICS_HOST=0.0.0.0and publish the metrics port only on the internal network.
Data
The data is US federal and state contract opportunities, grants, jobs, federal contract awards and grant awards from matchawards.com. Only public posts are returned, and each result links back to its page on matchawards.com.
Contract and grant searches return open items by default: a response deadline of today or later, US Eastern. An item with no deadline counts as closed.
Job search covers the last 30 days.
Results come newest first. When
has_moreis true, the model can ask for the next page withcursor.A keyword search (contracts, grants or awards) without
naicswhose first page is all there is (nocursorpassed, nohas_more, nowarning) and holds fewer than 5 rows, and fewer than itslimit, says in itsnotethat the keyword matches titles only, and thatlookup_naicsgives the codes for a line of work.A narrow filter (an agency, a rare name) can scan the newest records without a match while more exist further back; the result's
warningthen says so and gives thecursorto continue with.If the first request of a search fails, the tool returns an error. If a later page fails, the tool returns the rows it already has, with a
warningthat says why it stopped.A result row in an unexpected format is left out and counted in
warning; the rest of the search is returned.Each API call times out after 10 seconds.
The API is rate-limited per IP: 60 requests per minute, plus a burst limit of 20 requests per 5 seconds. One search call can use up to three requests when the filters are narrow and results are sparse, plus one to look up an agency name (cached for 10 minutes). When the limit is reached, the tool returns an error that tells the model how many seconds to wait.
Development
python -m venv .venv
.venv/bin/pip install -e ".[test]"
.venv/bin/pytestThe tests make no network calls.
tests/golden/prompts.json lists prompts with the tool a model should pick (or none), for manual checks of tool selection in a client.
License
MIT. See LICENSE.
Available Tools
9 toolsassess_opportunityAssess an opportunityARead-onlyIdempotentInspect
Use this when the user asks if one opportunity is a good fit for them, whether to bid, or wants a bid brief: days left, set-aside match, similar past awards at the same agency, the firms that usually win such work and the usual award value.
Pass the id from a search result, and optionally my_naics (up to 10 codes) and my_set_aside to check the fit. Similar awards come from MatchAwards' award records for the same NAICS at the same agency, or at all agencies when that agency has none (basis says which); sample gives the dates covered and whether every matching record was read, so state that span rather than a longer one. Award records may be incomplete, so say so; ai text is AI-generated, so label it. Never call a firm the incumbent: these are firms that won similar work. Example: id="117412688283576625".
Print url, sam_url and each award's url and source_url as markdown links.
Do not use for searching (search_contracts, search_grants), for finding partners (find_teaming_partners) or for job postings.
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | The id field of a search result. | |
| my_naics | No | Up to 10 six-digit NAICS codes, comma-separated, e.g. '236220' or '541511,541512'. A record matches on its own codes (matched_naics says which). Unknown codes are ignored. | |
| my_set_aside | No | The user's set-aside code, e.g. 'SBA' or 'SDVOSBC', to compare with the opportunity's. |
Output Schema
| Name | Required | Description |
|---|---|---|
| ai | No | AI-generated notes on this opportunity, or null. Label them as AI-generated when you show or quote them. |
| note | No | |
| as_of | No | |
| basis | No | 'same NAICS and agency', 'same NAICS, all agencies', or null (no NAICS). |
| sample | Yes | |
| warning | No | Set when the award lookup was cut short; the figures cover what was read. |
| naics_match | No | Whether the opportunity's NAICS is one of my_naics; null when my_naics was not given. |
| opportunity | Yes | |
| coverage_note | No | |
| similar_awards | Yes | The 5 newest similar awards. |
| set_aside_match | No | Whether the opportunity is set aside for my_set_aside; null when it was not given. An opportunity without a set-aside is open to all firms. |
| typical_winners | Yes | Up to 5 firms that won similar work, most awards first, then largest total. |
| award_value_range | No | Quartiles of the similar awards' amounts (USD); null with fewer than 3 amounts. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover the safety profile (readOnly, idempotent, non-destructive, closed-world), and the description adds substantive context beyond them: MatchAwards data falls back to all agencies when the agency has none (with basis/sample indicating coverage), award records may be incomplete, ai text is AI-generated and must be labeled, and firms must not be called incumbents. It also specifies required output formatting (markdown links for url, sam_url, and each award's url/source_url).
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the trigger and the result payload, then parameters, then data caveats, then formatting, then exclusions — a logical order with no filler sentences. It is dense and long, but nearly every clause carries operational value; only the explicit listing of output contents overlaps the output schema.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Even though an output schema exists, the description adds the interpretive layer an agent needs: the basis/sample caveat tells it how to phrase coverage honestly, and the incumbent/AI-generated rules prevent mischaracterization of results. Combined with the explicit exclusions and formatting requirements, nothing material is missing for correct invocation or reporting.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and each parameter already carries a description, so the baseline is 3. The prose restates the same guidance ('pass the id from a search result', 'my_naics up to 10 codes', 'my_set_aside to check the fit') without adding syntax or semantics beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (assess) and resource (one opportunity) and enumerates the output payload: days left, set-aside match, similar past awards at the same agency, the firms that usually win, and usual award value. It also names the sibling tools it is not (search_contracts, search_grants, find_teaming_partners), so an agent can route without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Opens with explicit trigger conditions ('when the user asks if one opportunity is a good fit, whether to bid, or wants a bid brief') and closes with explicit exclusions naming the alternative tools for searching, partner-finding, and job postings. When-to-use, when-not-to-use, and alternatives are all present.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
find_teaming_partnersFind teaming partnersARead-onlyIdempotentInspect
Use this when the user wants possible teaming partners, subcontractors or primes for one opportunity: firms that won similar awards in the same state or at the same agency.
Pass the id from a search result, and optionally role (sub, prime or any). Returns up to 10 firms from MatchAwards' award records (same NAICS in the opportunity's state, then at its agency, else anywhere; sample gives the dates covered and whether every matching record was read), ranked by how many similar awards they won, each with a why line; get_company(uei) shows the history of a firm with a uei. No contact details. Award records may be incomplete, so say so; ai_subcontracting is AI-generated, so label it. Never call a firm the incumbent. Example: id="117412688283576625".
Print the opportunity's url and sam_url as markdown links.
Do not use for searching awards in general (search_awards), for one firm's history (get_company) or for job postings.
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | The id field of a search result. | |
| role | No | sub: the user looks for subcontractors (smaller typical awards first among equals); prime: for a prime to join (larger first); any (default). | any |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| role | Yes | |
| as_of | No | |
| sample | Yes | |
| warning | No | Set when the award lookup was cut short; the list covers what was read. |
| partners | Yes | Up to 10 firms that won similar work. |
| opportunity | Yes | |
| coverage_note | No | |
| ai_subcontracting | No | AI-generated notes on subcontracting angles for this opportunity, or null. Label them as AI-generated when you show them. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations cover the safety profile (readOnly, idempotent, non-destructive), and the description adds substantial context beyond that: the 10-firm cap, the ranking logic and fallback order, that contact details are absent, that award records may be incomplete, and that ai_subcontracting is AI-generated and must be labeled. It also gives a hard usage rule ('Never call a firm the incumbent'), which is exactly the kind of behavioral constraint annotations cannot express.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Dense but front-loaded: the trigger condition leads, followed by behavior, caveats, and exclusions. Most sentences earn their place, though it is on the long side and the markdown-link formatting instruction is a slightly jarring insertion mid-paragraph.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, the description needn't explain return values, and it instead covers everything else an agent needs: fallback ordering, ranking, data-quality caveats, labeling rules, and the required URL formatting. Nothing material for correct invocation is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so both parameters are already fully documented, including the detailed role enum semantics. The description's 'Pass the id from a search result, and optionally role' largely restates the schema; the example id="117412688283576625" adds a small format cue. Baseline 3 is appropriate since the schema does the heavy lifting.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Find teaming partners... firms that won similar awards in the same state or at the same agency') with a precise scope. It distinguishes itself from sibling tools by naming search_awards and get_company as separate concerns. An agent can identify this tool without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Opens with an explicit trigger ('Use this when the user wants possible teaming partners... for one opportunity') and closes with explicit exclusions naming the alternatives ('Do not use for... search_awards... get_company... or for job postings'). Both when-to-use and when-not-to-use are covered with named siblings.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_companyGet company award historyARead-onlyIdempotentInspect
Use this when the user wants one firm's government award history and its UEI is known, e.g. from a search_awards row.
Returns the firm's name and other spellings, location (city, state, country), totals for contract and grant awards, first and last award dates, top NAICS codes and agencies, and its 10 most recent awards. No contact details. Award records may be incomplete, so say so. Example: uei="C3KXNLTAAY98".
Print each recent award's url and source_url as markdown links.
Do not use for finding firms by name or NAICS (search_awards) or for contact details (none are available).
| Name | Required | Description | Default |
|---|---|---|---|
| uei | Yes | 12-character UEI, e.g. 'C3KXNLTAAY98'. |
Output Schema
| Name | Required | Description |
|---|---|---|
| uei | Yes | |
| duns | No | |
| name | No | |
| note | No | |
| as_of | No | |
| totals | No | |
| location | No | |
| top_naics | No | From contract awards only. |
| other_names | No | Other spellings in award records. |
| top_agencies | No | |
| coverage_note | No | |
| recent_awards | No | Up to 10, newest first. |
| last_award_date | No | |
| first_award_date | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare read-only, idempotent, non-destructive, closed-world behavior, so the safety profile is covered. The description adds genuinely non-obvious context: no contact details are returned, and award records may be incomplete so the agent should caveat results. It stops short of describing pagination or truncation of the top NAICS/agency lists.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loads the when-to-use condition and keeps the exclusion last, which is good ordering. It is slightly long because it enumerates returned fields and adds a presentation instruction ('Print each recent award's url and source_url as markdown links'), but each sentence carries actionable content.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, the description does not need to enumerate return values, yet it still lists them usefully and covers the main risk (incomplete records) plus the UEI provenance. Nothing critical for correct invocation is missing, although the return list is partly redundant with the output schema.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the schema already supplies the pattern and the exact same example ('C3KXNLTAAY98'). The description's only addition is provenance ('e.g. from a search_awards row'), which is mildly useful but does not go beyond the schema in any substantive way, so the baseline of 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('one firm's government award history') and explicitly scopes it to when the UEI is known. It distinguishes itself from search_awards by naming the alternative for name/NAICS lookup, so an agent can route without opening either schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives an explicit trigger ('when the user wants one firm's ... and its UEI is known, e.g. from a search_awards row') plus explicit exclusions ('Do not use for finding firms by name or NAICS (search_awards) or for contact details'). Both the when and the when-not are named with their alternatives.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_opportunityGet opportunity detailsARead-onlyIdempotentInspect
Use this when the user wants the details of one search result, or who to contact about it.
Pass the id from a search result. Returns the result's row plus its description (up to 2000 characters), published points of contact (name and title only) and, when MatchAwards has them, ai: AI-generated notes (subcontracting and large-prime angles) and a summary. AI text is AI-generated: label it as such whenever you quote it. Example: id="117402307943537453".
Print url, and sam_url for federal notices, as markdown links.
Do not use for searching (search_contracts, search_grants, search_jobs), for fit or bid/no-bid questions (assess_opportunity) or for contact details beyond the published names and titles.
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | The id field of a search result. |
Output Schema
| Name | Required | Description |
|---|---|---|
| ai | No | AI-generated notes on this opportunity, or null. Label them as AI-generated when you show or quote them. |
| id | Yes | Pass to get_opportunity for the description and contacts. |
| url | No | The posting on matchawards.com. Show it as a markdown link. |
| city | No | |
| note | No | |
| type | No | federal, state, grant or job. |
| as_of | No | |
| naics | No | On state notices (type state) the naics code itself is AI-assigned. |
| state | No | |
| title | No | |
| agency | No | |
| office | No | |
| sam_url | No | The SAM.gov notice (federal only). Show it as a markdown link. |
| contacts | No | Published points of contact; [] if none. |
| agency_id | No | The agency's MatchAwards id. |
| posted_at | No | Date, YYYY-MM-DD. |
| set_aside | No | |
| description | No | Up to 2000 characters. |
| matched_naics | No | Only when you filtered by naics: the requested codes this record carries: its own code (in naics) or, for a grant or job, another code of its program or occupation. |
| ai_matched_naics | No | Only when you filtered by naics: requested codes this contract notice matched only because MatchAwards' AI suggested them, not its own codes. Call such a match AI-suggested. |
| no_fixed_deadline | No | true: the deadline is a placeholder date. |
| response_deadline | No | Date, YYYY-MM-DD; null if none. |
| solicitation_number | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/non-destructive, and the description adds substantially more: the 2000-character truncation of description, that AI notes are optional ('when MatchAwards has them'), and a required labeling caveat for AI-generated text. It also specifies output presentation duties (markdown links for url/sam_url), which annotations cannot convey.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the when-to-use sentence, followed by ordered operational notes (id provenance, return contents, AI labeling, link formatting, exclusions). Every sentence is actionable, though the formatting directive is a slight extension beyond core tool semantics.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, yet the description still summarizes the payload and flags the AI-generated caveat, which is the one thing an agent could misuse. Combined with explicit routing to siblings, an agent has everything needed to call this correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the pattern is documented, so the description only needs to add value; it does by stating the id must come from a search result and giving a concrete example id. Marginal gain over the schema, so not a 5.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('details of one search result') and explicitly scopes what is returned (row, description up to 2000 chars, contacts, optional AI notes). It is clearly distinguishable from search_* and assess_opportunity because it retrieves a single known id rather than searching or evaluating.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives an explicit trigger ('when the user wants the details of one search result, or who to contact about it'), explicit anti-triggers (not for searching, not for fit/bid questions, not for deeper contact details), and names the alternatives to use instead (search_contracts, search_grants, search_jobs, assess_opportunity).
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
lookup_naicsLook up NAICS codesARead-onlyIdempotentInspect
Use this when the user describes a line of work in words and a NAICS code is needed, or asks what a code or code prefix covers.
query is words (2 to 100 characters; common terms such as cybersecurity or janitorial list their usual codes first) or 2 to 6 digits (every six-digit code starting with them). limit is 1 to 25, default 10. Pass the codes as naics to search_contracts, search_grants, search_jobs or search_awards.
Print each opportunities_url that is set as a markdown link.
Do not use for searching opportunities or awards themselves (the search tools take the codes).
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Number of codes to return, 1 to 25. | |
| query | Yes | Words, e.g. 'cybersecurity', or the first 2 to 6 digits of a code, e.g. '5415'. |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| as_of | No | |
| results | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/non-destructive/closed-world, so safety is covered. The description goes beyond them by disclosing ranking behavior ('common terms such as cybersecurity or janitorial list their usual codes first'), prefix-expansion behavior for digits, and a required output presentation step (print each opportunities_url as a markdown link).
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the triggering condition and free of filler, but the middle paragraph is tightly packed and mixes query semantics, limits, and cross-tool routing into dense clauses. Still, every sentence carries information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists so return values need no explanation, annotations cover the safety profile, and the description supplies usage triggers, query semantics, routing to consumers, and output formatting. Nothing an agent needs to call this correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so baseline is 3, but the description adds real meaning beyond the schema: the 2-100 character bound is restated with a concrete purpose, and it explains that a 2-6 digit query returns every six-digit code starting with those digits. The 'usual codes first' ordering for common words is also semantics the schema does not carry.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The opening sentence names a specific verb+resource (look up a NAICS code) and states the two triggering intents: describing a line of work in words, or asking what a code/prefix covers. It is clearly distinguishable from the sibling search_* tools, which it explicitly routes codes to.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit when-to-use ('the user describes a line of work in words and a NAICS code is needed'), explicit when-not-to-use ('Do not use for searching opportunities or awards themselves'), and names the alternative tools to hand codes to. Nothing is left to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_awardsSearch past awardsARead-onlyIdempotentInspect
Use this when the user asks who won government contracts or grants: past awards by NAICS, agency, state, awardee name or UEI, with amounts and dates.
Filters: type (contract_award, grant_award or any), naics (up to 10 codes), agency (a name; the note says which agency it matched), state, awardee (part of a name), uei, keyword (matched in the title; a grant award's title is its recipient's name), awarded_within_days (1 to 1095, default 365), limit (1 to 50, default 20). If has_more is true, call again with cursor=next_cursor and the same filters. Example: naics="541512", agency="Department of Veterans Affairs". Pass several NAICS codes in one call.
Award records may be incomplete, so say so. Head a grant award with its awardee name and CFDA number, not its title. Pass an awardee's uei to get_company for its full history.
Print each award's url and source_url as markdown links.
Do not use for open opportunities to bid on (search_contracts, search_grants) or for one firm's history when its UEI is known (get_company).
| Name | Required | Description | Default |
|---|---|---|---|
| uei | No | The awardee's 12-character UEI. | |
| type | No | contract_award (federal contracts), grant_award, or any (both). | any |
| limit | No | Number of results to return, 1 to 50. | |
| naics | No | Up to 10 six-digit NAICS codes, comma-separated, e.g. '236220' or '541511,541512'. A record matches on its own codes (matched_naics says which). Unknown codes are ignored. | |
| state | No | Two-letter US state code of the place of performance. | |
| agency | No | Agency name, e.g. 'Department of Veterans Affairs'; matched to the closest MatchAwards agency, which the result note names. | |
| cursor | No | next_cursor from the previous response. Pass it with the same filters to get the next page. | |
| awardee | No | Part of the awardee's name, 2 to 100 characters. | |
| keyword | No | 2 to 100 characters matched against the title only. | |
| awarded_within_days | No | Only awards from the last N days, 1 to 1095 (default 365). |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| as_of | No | When the API generated the results (ISO time). |
| results | Yes | |
| warning | No | Set when paging stopped early; the results so far are still valid. |
| has_more | Yes | true: call again with cursor=next_cursor and the same filters. |
| next_cursor | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/non-destructive, so the description correctly spends its words on what annotations cannot express: pagination behavior ('If has_more is true, call again with cursor=next_cursor and the same filters'), a data-quality caveat ('Award records may be incomplete, so say so'), and the agency-name fuzzy-match note. That is substantive behavioral context beyond the structured fields.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded purpose sentence, then a tight filter inventory with defaults, then pagination, then caveats and routing exclusions. It is long, but every block carries distinct information and the trailing markdown-link instruction is a concrete output requirement rather than filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values rightly go unexplained, yet the description still covers the operational essentials an agent needs: has_more/next_cursor continuation, ambiguous-agency handling, incomplete-record disclosure, and the sibling boundary. For a 10-parameter open search with 0 required parameters, that is complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds semantics the schema does not: a grant award's title is its recipient's name (which changes how keyword behaves), the matched_naics/matched-agency result notes, and the guidance to batch several NAICS codes in one call. It re-enumerates the filters somewhat redundantly, keeping it at 4 rather than 5.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
'past awards by NAICS, agency, state, awardee name or UEI, with amounts and dates' is a specific verb+resource+filter scope, and the description explicitly contrasts itself with search_contracts, search_grants and get_company, so it is distinguishable from siblings without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It states the triggering condition ('when the user asks who won government contracts or grants') and names the exclusions with the alternatives to use instead ('Do not use for open opportunities to bid on (search_contracts, search_grants) or for one firm's history when its UEI is known (get_company)'). Nothing is left to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_contractsSearch government contractsARead-onlyIdempotentInspect
Use this when the user wants US federal or state government contract opportunities to bid on: bids, RFPs, solicitations, set-asides.
Filters: naics (up to 10 codes), state, keyword (title only), set_aside (federal only), type (federal or state), posted_within_days (1 to 180), open (default true), agency (a name; the note says which agency it matched), limit (1 to 50, default 20). If has_more is true, call again with cursor=next_cursor and the same filters. Example: naics="238160", state="VA", keyword="roof". Pass several NAICS codes in one call.
Print each result's url, and sam_url for federal notices, as markdown links.
Do not use for grants (search_grants), jobs (search_jobs) or one result's details (get_opportunity).
| Name | Required | Description | Default |
|---|---|---|---|
| open | No | true (the default): only items with a response deadline of today or later, US Eastern; an item with no deadline counts as closed. false: include closed ones. | |
| type | No | Narrow to federal or to state contracts. Leave empty for both. | |
| limit | No | Number of results to return, 1 to 50. | |
| naics | No | Up to 10 six-digit NAICS codes, comma-separated, e.g. '236220' or '541511,541512'. A notice matches on its own codes (matched_naics says which), or through a code MatchAwards' AI suggested: that one is listed in ai_matched_naics, so call the match AI-suggested. Unknown codes are ignored. | |
| state | No | Two-letter US state code, e.g. 'VA'. Many federal notices have no state, so this filter skips them. | |
| agency | No | Agency name, e.g. 'Department of Veterans Affairs'; matched to the closest MatchAwards agency, which the result note names. | |
| cursor | No | next_cursor from the previous response. Pass it with the same filters to get the next page. | |
| keyword | No | 2 to 100 characters matched against the title only. | |
| set_aside | No | Set-aside code, e.g. 'SBA' or 'SDVOSBC'. Federal contracts only. | |
| posted_within_days | No | Only items posted in the last N days, 1 to 180 (default 180). |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| as_of | No | When the API generated the results (ISO time). |
| results | Yes | |
| warning | No | Set when paging stopped early; the results so far are still valid. |
| has_more | Yes | true: call again with cursor=next_cursor and the same filters. |
| next_cursor | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/non-destructive, so the description correctly spends its words elsewhere: it discloses the pagination contract (has_more, next_cursor, same filters) and the effective default open=true that the schema alone leaves as null. It also discloses two behaviors invisible in structured fields — that agency names are matched to the closest MatchAwards agency and reflected in the result note, and that results should be rendered as markdown links with sam_url for federal notices. Minor gap: no note on result volume or latency expectations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with purpose, then a compact filters block, then pagination, then presentation, then exclusions — a sensible order with no filler. The filter enumeration largely duplicates the schema and is the one section that could be trimmed without loss.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 10-parameter tool with an output schema and rich annotations, everything an agent needs is present: routing in and out, filter interactions that surprise people (state skips federal notices without one, set_aside is federal-only), the paging loop, and output presentation. Return shape is correctly left to the output schema.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the schema already documents every parameter including the deadline-based meaning of open and the NAICS pattern. The description mostly restates the same list; the genuine additions are the example, "Pass several NAICS codes in one call", and stating open's effective default as true where the schema shows default null. That is real but incremental value over a fully documented schema, so the baseline 3 is right.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
"Use this when the user wants US federal or state government contract opportunities to bid on: bids, RFPs, solicitations, set-asides" gives a specific verb-plus-resource plus the vocabulary an agent will actually see from users. It names the exact siblings it is not (search_grants, search_jobs, get_opportunity), so routing is unambiguous without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit when-to-use clause up front, explicit exclusions mapped to named alternatives (grants, jobs, single-record details), and an explicit continuation rule for pagination with has_more/cursor. A worked example (naics="238160", state="VA", keyword="roof") anchors the intended call shape.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_grantsSearch grantsARead-onlyIdempotentInspect
Use this when the user wants US federal grants or funding opportunities.
Filters: naics (up to 10 codes), keyword (title only), posted_within_days (1 to 180), open (default true: open grants of any age), limit (1 to 50, default 20). Grant notices carry no state. If has_more is true, call again with cursor=next_cursor and the same filters. Example: keyword="broadband". Pass several NAICS codes in one call.
Open status may lag for grants that closed recently, so check response_deadline.
Print each result's url as a markdown link.
Do not use for contracts to bid on (search_contracts) or jobs (search_jobs).
| Name | Required | Description | Default |
|---|---|---|---|
| open | No | true (the default): only items with a response deadline of today or later, US Eastern; an item with no deadline counts as closed. false: include closed ones. | |
| limit | No | Number of results to return, 1 to 50. | |
| naics | No | Up to 10 six-digit NAICS codes, comma-separated, e.g. '236220' or '541511,541512'. A record matches on its own codes (matched_naics says which). Unknown codes are ignored. | |
| cursor | No | next_cursor from the previous response. Pass it with the same filters to get the next page. | |
| keyword | No | 2 to 100 characters matched against the title only. | |
| posted_within_days | No | Only items posted in the last N days, 1 to 180 (default 180). |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| as_of | No | When the API generated the results (ISO time). |
| results | Yes | |
| warning | No | Set when paging stopped early; the results so far are still valid. |
| has_more | Yes | true: call again with cursor=next_cursor and the same filters. |
| next_cursor | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already establish read-only, idempotent, closed-world behavior, but the description adds real operational context not in annotations: cursor/has_more pagination protocol, the warning that open status can lag so response_deadline should be checked, and that grant notices carry no state. The one weak spot is the phrase 'open grants of any age', which is vaguer and looser than the schema's precise deadline-based definition (no deadline counts as closed).
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded trigger, then a compact filter summary, pagination rule, a caveat, output formatting, and exclusions — each block earns its place. The parenthetical filter list mildly duplicates the schema's field descriptions but functions as a single-glance summary rather than padding.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be enumerated, yet the description still covers the one thing the schema cannot: the pagination loop (has_more/next_cursor with identical filters) and result presentation. For a six-parameter, zero-required search tool, an agent has everything needed to call it correctly on the first and subsequent pages.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3; the description still adds value by flagging keyword as title-only, restating the 1-180/1-50 bounds as quick reference, and adding the strategy hint 'pass several NAICS codes in one call' that is absent from the schema. No added detail on how posted_within_days interacts with open, hence not a 5.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Opening line names a specific verb (search) and resource (US federal grants/funding opportunities), and the closing line explicitly separates it from search_contracts and search_jobs. An agent can route correctly without inspecting any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives an explicit when-to-use trigger ('when the user wants US federal grants'), explicit exclusions with named alternative tools for contracts and jobs, plus operational guidance to batch NAICS codes in one call. Nothing is left to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_jobsSearch jobsARead-onlyIdempotentInspect
Use this when the user is looking for a job, or for hiring signals, among postings from the last 30 days.
Filters: naics (up to 10 codes), state, keyword (title only), posted_within_days (posted within the last 1 to 30 days, default 30), limit (1 to 50, default 20). Rows carry title, agency (the employer), state, city, posted_at and url. If has_more is true, call again with cursor=next_cursor and the same filters. Example: keyword="electrician", state="TX". Pass several NAICS codes in one call.
Print each result's url as a markdown link.
Do not use for contracts (search_contracts) or grants (search_grants).
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Number of results to return, 1 to 50. | |
| naics | No | Up to 10 six-digit NAICS codes, comma-separated, e.g. '236220' or '541511,541512'. A record matches on its own codes (matched_naics says which). Unknown codes are ignored. | |
| state | No | Two-letter US state code, e.g. 'VA'. Many federal notices have no state, so this filter skips them. | |
| cursor | No | next_cursor from the previous response. Pass it with the same filters to get the next page. | |
| keyword | No | 2 to 100 characters matched against the title only. | |
| posted_within_days | No | Only jobs posted within the last N days, 1 to 30 (default 30). |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| as_of | No | When the API generated the results (ISO time). |
| results | Yes | |
| warning | No | Set when paging stopped early; the results so far are still valid. |
| has_more | Yes | true: call again with cursor=next_cursor and the same filters. |
| next_cursor | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover the safety profile (readOnly, idempotent, non-destructive, closed-world), so the burden is lighter. The description still adds value beyond them: the pagination contract via has_more/next_cursor, the filter semantics (state filter drops records with no state), and the presentation instruction to emit urls as markdown links. It does not disclose rate limits or result-ordering behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the trigger condition, then a compact filter list, then return fields, then pagination, then exclusions. Every sentence carries a distinct instruction and none are redundant with each other.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, the description need not enumerate return shapes, yet it still names the key row fields for planning. Pagination, filter interactions, and routing to sibling tools are all covered, leaving nothing an agent needs to call this correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so baseline is 3, but the description adds operational guidance the schema lacks: the example values, the instruction to pass several NAICS codes in one call, and the note that matched_naics identifies which code hit. It also clarifies that 'agency' means the employer, which is not derivable from the schema alone.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Opens with a specific trigger ('the user is looking for a job, or for hiring signals') plus a concrete resource and scope ('postings from the last 30 days'). It also names the sibling tools it is not (search_contracts, search_grants), so an agent can disambiguate without opening schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit when-to-use, explicit when-not-to-use with the alternative tools named, and a worked example (keyword="electrician", state="TX"). The pagination rule ('call again with cursor=next_cursor and the same filters') is a clear operational condition.
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.
9 tool updates
v0.1.0- First observed
assess_opportunity - First observed
find_teaming_partners - First observed
get_company - First observed
get_opportunity - First observed
lookup_naics - First observed
search_awards - First observed
search_contracts - First observed
search_grants - First observed
search_jobs
TDQS
Scored across 9 tools
Each tool targets a distinct resource-and-action pair (search_contracts vs search_grants vs search_jobs vs search_awards, plus single-record get_opportunity/get_company). Descriptions explicitly include 'Do not use for...' routing hints that name the correct alternative tool, eliminating overlap.
All nine tools follow a consistent snake_case verb_noun pattern (search_*, get_*, lookup_naics, assess_opportunity, find_teaming_partners). Verbs are used predictably: search_ for filtered lists, get_ for single records by id/UEI.
Nine tools is well within the ideal 3-15 range, and each earns its place in a coherent workflow (find codes, search each opportunity type, inspect details, evaluate fit, find partners). No tool appears redundant or padded.
The surface covers the full discovery lifecycle: NAICS resolution, search across contracts/grants/jobs/awards, record details, firm history, bid fit, and teaming partners. Minor gaps remain, such as no per-award detail tool, no saved searches or deadline/alerts view, and no agency lookup beyond name matching.
Maintenance
Related MCP Connectors
Search US grants + federal contracts (Grants.gov + SAM.gov) from any LLM.
Search U.S. federal contract opportunities (RFPs, bids, awards) from SAM.gov official data.
Search U.S. federal, state, and local government procurement data and intelligence.
Government tender search for AI agents. UK, EU and US procurement opportunities.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables research of federal contract awards, market opportunities, and competitive landscapes using the USASpending.gov API. It provides specialized tools for AI agents to analyze government spending trends, identify incumbents, and search contractor details.MIT
- AlicenseNot gradedqualityFmaintenanceEnables AI agents to search and analyze federal government contract opportunities and awards from SAM.gov and USASpending.gov, with tools for contract search, detail, agency spending, and trending sectors.MIT
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to search federal contracts, analyze agency spending, track competitor wins, and monitor small business set-aside opportunities using SAM.gov, USASpending.gov, and FPDS data.1-
- AlicenseAqualityCmaintenanceEnables AI agents to find, score, and monitor government contract opportunities across UK, EU, and US with AI-powered relevance scoring.2103 npm1MIT