govconapi-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- govconapi_mcp-0.1.0/.gitignore +10 -0
- govconapi_mcp-0.1.0/CLAUDE_CONFIG_EXAMPLE.json +10 -0
- govconapi_mcp-0.1.0/PKG-INFO +134 -0
- govconapi_mcp-0.1.0/README.md +111 -0
- govconapi_mcp-0.1.0/govconapi_mcp/__init__.py +2 -0
- govconapi_mcp-0.1.0/govconapi_mcp/server.py +327 -0
- govconapi_mcp-0.1.0/pyproject.toml +38 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: govconapi-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for GovCon API – federal contract opportunities, awards, and exclusions in Claude/ChatGPT/any MCP client
|
|
5
|
+
Project-URL: Homepage, https://govconapi.com
|
|
6
|
+
Project-URL: Documentation, https://govconapi.com/api-guide
|
|
7
|
+
Project-URL: Repository, https://github.com/rozetyp/sam-api
|
|
8
|
+
Author-email: GovCon API <support@govconapi.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: claude,federal-contracts,govcon,mcp,model-context-protocol,sam-gov
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx>=0.27.0
|
|
21
|
+
Requires-Dist: mcp>=1.0.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# GovCon API — MCP Server
|
|
25
|
+
|
|
26
|
+
Federal contract opportunities, awards, and exclusions inside Claude Desktop, ChatGPT, or any MCP-compatible client.
|
|
27
|
+
|
|
28
|
+
Ask in natural language:
|
|
29
|
+
|
|
30
|
+
> "Find DoD cybersecurity solicitations posted this month over $1M."
|
|
31
|
+
|
|
32
|
+
> "What's the canonical SAM.gov agency name for FEMA?"
|
|
33
|
+
|
|
34
|
+
> "Has Acme Corp ever been excluded from federal contracts?"
|
|
35
|
+
|
|
36
|
+
> "Show me everything that changed since yesterday."
|
|
37
|
+
|
|
38
|
+
Powered by [GovCon API](https://govconapi.com) — a clean SAM.gov data layer.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install govconapi-mcp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You'll need:
|
|
49
|
+
|
|
50
|
+
- Python 3.10+
|
|
51
|
+
- A GovCon API key — [free 14-day trial](https://govconapi.com), no credit card
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Configure Claude Desktop
|
|
56
|
+
|
|
57
|
+
Add to your Claude Desktop config file:
|
|
58
|
+
|
|
59
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
60
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"mcpServers": {
|
|
65
|
+
"govconapi": {
|
|
66
|
+
"command": "govconapi-mcp",
|
|
67
|
+
"env": {
|
|
68
|
+
"GOVCONAPI_KEY": "gca_your_key_here"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Restart Claude Desktop. You'll see the GovCon tools appear in the tool picker.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Tools
|
|
80
|
+
|
|
81
|
+
| Tool | What it does |
|
|
82
|
+
|------|--------------|
|
|
83
|
+
| `search_opportunities` | Search SAM.gov opportunities by NAICS, PSC, agency, state, keywords, set-aside, dates, value range |
|
|
84
|
+
| `get_opportunity` | Fetch one opportunity by notice_id (50+ fields) |
|
|
85
|
+
| `recent_changes` | Incremental sync — what's new since a timestamp |
|
|
86
|
+
| `lookup_agency` | Resolve "FEMA" / "DoD" / "VA" to canonical SAM.gov agency strings |
|
|
87
|
+
| `check_exclusion` | Debarment screening by name, UEI, or CAGE code |
|
|
88
|
+
| `search_awards` | Find who won contracts (company, amount, date) |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Example Claude prompts
|
|
93
|
+
|
|
94
|
+
- *"Search for HUBZone-set-aside opportunities in NAICS 541330 posted after April 1."*
|
|
95
|
+
- *"What agency string should I use to filter for all FEMA contracts?"*
|
|
96
|
+
- *"Find Award Notice records over $5M in California from the last 30 days."*
|
|
97
|
+
- *"Has [vendor name] ever appeared on the exclusions list?"*
|
|
98
|
+
- *"What's changed in the federal contracts dataset since 2026-04-12?"*
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Plan limits
|
|
103
|
+
|
|
104
|
+
- **Free Trial:** 25 requests/day for 14 days
|
|
105
|
+
- **Developer ($19/mo):** 1,000 requests/hour, all filters, CSV export
|
|
106
|
+
- **Pro Bundle ($39/mo):** Everything in Developer + unlimited GovCon Contacts directory
|
|
107
|
+
|
|
108
|
+
See [govconapi.com/pricing](https://govconapi.com/pricing).
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Troubleshooting
|
|
113
|
+
|
|
114
|
+
**"GOVCONAPI_KEY environment variable is not set"**
|
|
115
|
+
Add the env var to your Claude Desktop MCP config (see above) and restart Claude.
|
|
116
|
+
|
|
117
|
+
**"Invalid API key"**
|
|
118
|
+
Get a new one at [govconapi.com](https://govconapi.com) — keys start with `gca_`.
|
|
119
|
+
|
|
120
|
+
**"At least one filter required"**
|
|
121
|
+
The `search_opportunities` tool requires at least one filter (naics, keywords, state, etc.). If you want a bulk dump, use `recent_changes` instead.
|
|
122
|
+
|
|
123
|
+
**Rate limited (429)**
|
|
124
|
+
Free trial: 25 calls/day. Developer: 1,000/hour. Wait or upgrade.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
|
131
|
+
|
|
132
|
+
## Support
|
|
133
|
+
|
|
134
|
+
[support@govconapi.com](mailto:support@govconapi.com)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# GovCon API — MCP Server
|
|
2
|
+
|
|
3
|
+
Federal contract opportunities, awards, and exclusions inside Claude Desktop, ChatGPT, or any MCP-compatible client.
|
|
4
|
+
|
|
5
|
+
Ask in natural language:
|
|
6
|
+
|
|
7
|
+
> "Find DoD cybersecurity solicitations posted this month over $1M."
|
|
8
|
+
|
|
9
|
+
> "What's the canonical SAM.gov agency name for FEMA?"
|
|
10
|
+
|
|
11
|
+
> "Has Acme Corp ever been excluded from federal contracts?"
|
|
12
|
+
|
|
13
|
+
> "Show me everything that changed since yesterday."
|
|
14
|
+
|
|
15
|
+
Powered by [GovCon API](https://govconapi.com) — a clean SAM.gov data layer.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install govconapi-mcp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
You'll need:
|
|
26
|
+
|
|
27
|
+
- Python 3.10+
|
|
28
|
+
- A GovCon API key — [free 14-day trial](https://govconapi.com), no credit card
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Configure Claude Desktop
|
|
33
|
+
|
|
34
|
+
Add to your Claude Desktop config file:
|
|
35
|
+
|
|
36
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
37
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"govconapi": {
|
|
43
|
+
"command": "govconapi-mcp",
|
|
44
|
+
"env": {
|
|
45
|
+
"GOVCONAPI_KEY": "gca_your_key_here"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Restart Claude Desktop. You'll see the GovCon tools appear in the tool picker.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Tools
|
|
57
|
+
|
|
58
|
+
| Tool | What it does |
|
|
59
|
+
|------|--------------|
|
|
60
|
+
| `search_opportunities` | Search SAM.gov opportunities by NAICS, PSC, agency, state, keywords, set-aside, dates, value range |
|
|
61
|
+
| `get_opportunity` | Fetch one opportunity by notice_id (50+ fields) |
|
|
62
|
+
| `recent_changes` | Incremental sync — what's new since a timestamp |
|
|
63
|
+
| `lookup_agency` | Resolve "FEMA" / "DoD" / "VA" to canonical SAM.gov agency strings |
|
|
64
|
+
| `check_exclusion` | Debarment screening by name, UEI, or CAGE code |
|
|
65
|
+
| `search_awards` | Find who won contracts (company, amount, date) |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Example Claude prompts
|
|
70
|
+
|
|
71
|
+
- *"Search for HUBZone-set-aside opportunities in NAICS 541330 posted after April 1."*
|
|
72
|
+
- *"What agency string should I use to filter for all FEMA contracts?"*
|
|
73
|
+
- *"Find Award Notice records over $5M in California from the last 30 days."*
|
|
74
|
+
- *"Has [vendor name] ever appeared on the exclusions list?"*
|
|
75
|
+
- *"What's changed in the federal contracts dataset since 2026-04-12?"*
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Plan limits
|
|
80
|
+
|
|
81
|
+
- **Free Trial:** 25 requests/day for 14 days
|
|
82
|
+
- **Developer ($19/mo):** 1,000 requests/hour, all filters, CSV export
|
|
83
|
+
- **Pro Bundle ($39/mo):** Everything in Developer + unlimited GovCon Contacts directory
|
|
84
|
+
|
|
85
|
+
See [govconapi.com/pricing](https://govconapi.com/pricing).
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Troubleshooting
|
|
90
|
+
|
|
91
|
+
**"GOVCONAPI_KEY environment variable is not set"**
|
|
92
|
+
Add the env var to your Claude Desktop MCP config (see above) and restart Claude.
|
|
93
|
+
|
|
94
|
+
**"Invalid API key"**
|
|
95
|
+
Get a new one at [govconapi.com](https://govconapi.com) — keys start with `gca_`.
|
|
96
|
+
|
|
97
|
+
**"At least one filter required"**
|
|
98
|
+
The `search_opportunities` tool requires at least one filter (naics, keywords, state, etc.). If you want a bulk dump, use `recent_changes` instead.
|
|
99
|
+
|
|
100
|
+
**Rate limited (429)**
|
|
101
|
+
Free trial: 25 calls/day. Developer: 1,000/hour. Wait or upgrade.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
|
108
|
+
|
|
109
|
+
## Support
|
|
110
|
+
|
|
111
|
+
[support@govconapi.com](mailto:support@govconapi.com)
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""GovCon API MCP server.
|
|
2
|
+
|
|
3
|
+
Exposes federal contract opportunities, awards, exclusions, and the agency
|
|
4
|
+
crosswalk as MCP tools so AI clients (Claude Desktop, etc.) can query
|
|
5
|
+
SAM.gov data through natural language.
|
|
6
|
+
|
|
7
|
+
Configuration: set GOVCONAPI_KEY in your client's MCP config (see README).
|
|
8
|
+
Get a free trial key at https://govconapi.com.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import json
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
from mcp.server.fastmcp import FastMCP
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
BASE_URL = os.environ.get("GOVCONAPI_BASE", "https://govconapi.com")
|
|
21
|
+
API_KEY = os.environ.get("GOVCONAPI_KEY")
|
|
22
|
+
TIMEOUT = 30.0
|
|
23
|
+
USER_AGENT = "govconapi-mcp/0.1.0"
|
|
24
|
+
|
|
25
|
+
mcp = FastMCP("govconapi")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _client() -> httpx.AsyncClient:
|
|
29
|
+
if not API_KEY:
|
|
30
|
+
raise RuntimeError(
|
|
31
|
+
"GOVCONAPI_KEY environment variable is not set. "
|
|
32
|
+
"Get a free trial key at https://govconapi.com and add it to your "
|
|
33
|
+
"MCP client configuration."
|
|
34
|
+
)
|
|
35
|
+
return httpx.AsyncClient(
|
|
36
|
+
base_url=BASE_URL,
|
|
37
|
+
headers={
|
|
38
|
+
"Authorization": f"Bearer {API_KEY}",
|
|
39
|
+
"User-Agent": USER_AGENT,
|
|
40
|
+
},
|
|
41
|
+
timeout=TIMEOUT,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _drop_none(d: dict) -> dict:
|
|
46
|
+
return {k: v for k, v in d.items() if v is not None and v != ""}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _get(path: str, params: dict | None = None) -> dict:
|
|
50
|
+
"""Issue a GET request and return parsed JSON, raising friendly errors."""
|
|
51
|
+
async with _client() as client:
|
|
52
|
+
try:
|
|
53
|
+
r = await client.get(path, params=_drop_none(params or {}))
|
|
54
|
+
except httpx.TimeoutException:
|
|
55
|
+
raise RuntimeError(f"GovCon API timed out after {TIMEOUT}s.")
|
|
56
|
+
except httpx.RequestError as e:
|
|
57
|
+
raise RuntimeError(f"Network error reaching GovCon API: {e}")
|
|
58
|
+
|
|
59
|
+
if r.status_code == 401:
|
|
60
|
+
raise RuntimeError(
|
|
61
|
+
"Invalid API key. Check GOVCONAPI_KEY in your MCP config. "
|
|
62
|
+
"Get a key at https://govconapi.com."
|
|
63
|
+
)
|
|
64
|
+
if r.status_code == 402:
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
"Free trial expired. Upgrade at https://govconapi.com/pricing."
|
|
67
|
+
)
|
|
68
|
+
if r.status_code == 429:
|
|
69
|
+
raise RuntimeError("Rate limit exceeded. Try again in a moment.")
|
|
70
|
+
if r.status_code == 400:
|
|
71
|
+
try:
|
|
72
|
+
detail = r.json().get("detail", r.text)
|
|
73
|
+
except Exception:
|
|
74
|
+
detail = r.text
|
|
75
|
+
raise RuntimeError(f"Bad request: {detail}")
|
|
76
|
+
if r.status_code >= 500:
|
|
77
|
+
raise RuntimeError(f"GovCon API error {r.status_code}. Please retry.")
|
|
78
|
+
if r.status_code == 404:
|
|
79
|
+
return {"data": [], "_status": 404, "_message": "Not found"}
|
|
80
|
+
|
|
81
|
+
return r.json()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ─── Tools ────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@mcp.tool()
|
|
88
|
+
async def search_opportunities(
|
|
89
|
+
naics: Optional[str] = None,
|
|
90
|
+
psc: Optional[str] = None,
|
|
91
|
+
naics_multiple: Optional[str] = None,
|
|
92
|
+
agency: Optional[str] = None,
|
|
93
|
+
keywords: Optional[str] = None,
|
|
94
|
+
state: Optional[str] = None,
|
|
95
|
+
set_aside: Optional[str] = None,
|
|
96
|
+
notice_type: Optional[str] = None,
|
|
97
|
+
posted_after: Optional[str] = None,
|
|
98
|
+
due_before: Optional[str] = None,
|
|
99
|
+
due_after: Optional[str] = None,
|
|
100
|
+
date_from: Optional[str] = None,
|
|
101
|
+
date_to: Optional[str] = None,
|
|
102
|
+
value_min: Optional[float] = None,
|
|
103
|
+
value_max: Optional[float] = None,
|
|
104
|
+
has_attachments: Optional[bool] = None,
|
|
105
|
+
sort_by: Optional[str] = None,
|
|
106
|
+
limit: int = 20,
|
|
107
|
+
offset: int = 0,
|
|
108
|
+
) -> str:
|
|
109
|
+
"""Search federal contract opportunities (SAM.gov data) with filters.
|
|
110
|
+
|
|
111
|
+
At least one filter is required. Use specific values:
|
|
112
|
+
- naics: 6-digit NAICS code, e.g. "541330" (engineering services)
|
|
113
|
+
- psc: 1-4 char Product Service Code, e.g. "D302" (IT services)
|
|
114
|
+
- naics_multiple: comma-separated NAICS codes, e.g. "541511,541512"
|
|
115
|
+
- agency: agency name substring (use full names, e.g. "FEDERAL EMERGENCY MANAGEMENT" not "FEMA"; use lookup_agency tool first)
|
|
116
|
+
- keywords: full-text search across title, agency, description (min 3 chars)
|
|
117
|
+
- state: 2-letter state code (CA, TX) or full name (California)
|
|
118
|
+
- set_aside: SBA, 8(a), SDVOSB, HZC, SBP, etc.
|
|
119
|
+
- notice_type: Solicitation, Award Notice, Presolicitation, Combined Synopsis/Solicitation, Sources Sought
|
|
120
|
+
- posted_after / due_before / due_after / date_from / date_to: YYYY-MM-DD
|
|
121
|
+
- value_min / value_max: USD amounts (only Award Notice records have values)
|
|
122
|
+
- has_attachments: true/false
|
|
123
|
+
- sort_by: posted_date, due_date, award_amount, title, agency
|
|
124
|
+
- limit: max 1000
|
|
125
|
+
|
|
126
|
+
Returns JSON with `data` (matching opportunities), `pagination`, and `filters_applied`.
|
|
127
|
+
For full-database sync use the recent_changes tool instead.
|
|
128
|
+
"""
|
|
129
|
+
params = {
|
|
130
|
+
"naics": naics, "psc": psc, "naics_multiple": naics_multiple,
|
|
131
|
+
"agency": agency, "keywords": keywords, "state": state,
|
|
132
|
+
"set_aside": set_aside, "notice_type": notice_type,
|
|
133
|
+
"posted_after": posted_after, "due_before": due_before, "due_after": due_after,
|
|
134
|
+
"date_from": date_from, "date_to": date_to,
|
|
135
|
+
"value_min": value_min, "value_max": value_max,
|
|
136
|
+
"has_attachments": has_attachments, "sort_by": sort_by,
|
|
137
|
+
"limit": limit, "offset": offset,
|
|
138
|
+
}
|
|
139
|
+
data = await _get("/api/v1/opportunities/search", params)
|
|
140
|
+
return json.dumps(data, indent=2, default=str)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@mcp.tool()
|
|
144
|
+
async def get_opportunity(notice_id: str) -> str:
|
|
145
|
+
"""Fetch a single contract opportunity by its notice_id.
|
|
146
|
+
|
|
147
|
+
Returns the full record including agency, contacts, description, attachments,
|
|
148
|
+
award data (if applicable), and 50+ structured fields.
|
|
149
|
+
"""
|
|
150
|
+
data = await _get(f"/api/v1/opportunities/{notice_id}")
|
|
151
|
+
return json.dumps(data, indent=2, default=str)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@mcp.tool()
|
|
155
|
+
async def recent_changes(
|
|
156
|
+
since: str,
|
|
157
|
+
limit: int = 100,
|
|
158
|
+
offset: int = 0,
|
|
159
|
+
) -> str:
|
|
160
|
+
"""List opportunities added or updated since a timestamp.
|
|
161
|
+
|
|
162
|
+
Use this for incremental sync instead of paginating through search results.
|
|
163
|
+
Keyset-based, so depth doesn't degrade performance.
|
|
164
|
+
|
|
165
|
+
- since: ISO 8601 timestamp, e.g. "2026-04-12T00:00:00Z"
|
|
166
|
+
- limit: max 1000 per page
|
|
167
|
+
|
|
168
|
+
Returns `data` (changed records), `pagination`, and `sync.server_time` —
|
|
169
|
+
save server_time and pass it as `since` on your next call.
|
|
170
|
+
"""
|
|
171
|
+
params = {"since": since, "limit": limit, "offset": offset}
|
|
172
|
+
data = await _get("/api/v1/opportunities/delta", params)
|
|
173
|
+
return json.dumps(data, indent=2, default=str)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@mcp.tool()
|
|
177
|
+
async def lookup_agency(query: str) -> str:
|
|
178
|
+
"""Resolve an agency acronym or partial name to canonical SAM.gov agency strings.
|
|
179
|
+
|
|
180
|
+
SAM.gov stores agency names like "HOMELAND SECURITY, DEPARTMENT OF.FEDERAL
|
|
181
|
+
EMERGENCY MANAGEMENT AGENCY..." not "FEMA". Use this to find the right
|
|
182
|
+
substring to use as the `agency` filter in search_opportunities.
|
|
183
|
+
|
|
184
|
+
- query: acronym (FEMA, DoD, NASA), partial name, or full agency name
|
|
185
|
+
|
|
186
|
+
Returns matching agencies grouped by canonical name with the suggested
|
|
187
|
+
filter value to use.
|
|
188
|
+
"""
|
|
189
|
+
# Acronym hints help map common shorthand to canonical phrases the data uses
|
|
190
|
+
HINTS = {
|
|
191
|
+
"fema": "federal emergency management",
|
|
192
|
+
"dod": "defense", "usda": "agriculture", "doj": "justice",
|
|
193
|
+
"dot": "transportation", "hhs": "health and human", "dhs": "homeland security",
|
|
194
|
+
"epa": "environmental protection", "nasa": "aeronautics", "nih": "health",
|
|
195
|
+
"va": "veterans", "irs": "internal revenue", "sec": "securities exchange",
|
|
196
|
+
"fbi": "investigation", "cia": "intelligence", "dea": "drug enforcement",
|
|
197
|
+
"atf": "alcohol tobacco", "nsa": "national security", "usps": "postal",
|
|
198
|
+
"faa": "aviation", "fcc": "communications", "fda": "food and drug",
|
|
199
|
+
"usaid": "international development", "ssa": "social security",
|
|
200
|
+
"cms": "medicare medicaid", "cdc": "disease control",
|
|
201
|
+
"osha": "occupational safety", "uscg": "coast guard",
|
|
202
|
+
"usaf": "air force", "usmc": "marine", "doe": "energy",
|
|
203
|
+
"hud": "housing urban", "doi": "interior", "dol": "labor",
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
raw = await _get("/api/agency-crosswalk")
|
|
207
|
+
rows = raw.get("data", [])
|
|
208
|
+
|
|
209
|
+
q = query.lower().strip()
|
|
210
|
+
expansion = HINTS.get(q)
|
|
211
|
+
|
|
212
|
+
def haystack(r: dict) -> str:
|
|
213
|
+
return (
|
|
214
|
+
f"{r.get('raw_agency_text','')} "
|
|
215
|
+
f"{r.get('canonical_agency','')} "
|
|
216
|
+
f"{r.get('department','')}"
|
|
217
|
+
).lower()
|
|
218
|
+
|
|
219
|
+
def matches(r: dict) -> bool:
|
|
220
|
+
h = haystack(r)
|
|
221
|
+
if expansion:
|
|
222
|
+
return expansion in h
|
|
223
|
+
if len(q) <= 4:
|
|
224
|
+
# Word-boundary match for short queries to avoid e.g. "EPA" in "dEPArtment"
|
|
225
|
+
import re
|
|
226
|
+
return bool(re.search(rf"(^|[^a-z]){re.escape(q)}($|[^a-z])", h))
|
|
227
|
+
return q in h
|
|
228
|
+
|
|
229
|
+
matched = [r for r in rows if matches(r)]
|
|
230
|
+
|
|
231
|
+
# Group by canonical name
|
|
232
|
+
groups: dict[str, dict] = {}
|
|
233
|
+
for row in matched:
|
|
234
|
+
key = row.get("canonical_agency") or row.get("raw_agency_text") or "(unknown)"
|
|
235
|
+
g = groups.setdefault(key, {
|
|
236
|
+
"canonical_agency": key,
|
|
237
|
+
"department": row.get("department", ""),
|
|
238
|
+
"suggested_filter_value": key,
|
|
239
|
+
"total_contracts": 0,
|
|
240
|
+
"variation_count": 0,
|
|
241
|
+
"raw_variations": [],
|
|
242
|
+
})
|
|
243
|
+
g["total_contracts"] += int(row.get("frequency") or 0)
|
|
244
|
+
g["variation_count"] += 1
|
|
245
|
+
g["raw_variations"].append({
|
|
246
|
+
"raw": row.get("raw_agency_text", ""),
|
|
247
|
+
"frequency": row.get("frequency", 0),
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
grouped = sorted(groups.values(), key=lambda x: -x["total_contracts"])
|
|
251
|
+
|
|
252
|
+
# Trim raw_variations to top 5 per group
|
|
253
|
+
for g in grouped:
|
|
254
|
+
g["raw_variations"] = sorted(g["raw_variations"], key=lambda x: -(x.get("frequency") or 0))[:5]
|
|
255
|
+
|
|
256
|
+
return json.dumps({
|
|
257
|
+
"query": query,
|
|
258
|
+
"expansion_used": expansion,
|
|
259
|
+
"match_count": len(matched),
|
|
260
|
+
"agency_count": len(grouped),
|
|
261
|
+
"agencies": grouped[:15],
|
|
262
|
+
}, indent=2, default=str)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@mcp.tool()
|
|
266
|
+
async def check_exclusion(
|
|
267
|
+
name: Optional[str] = None,
|
|
268
|
+
uei: Optional[str] = None,
|
|
269
|
+
cage_code: Optional[str] = None,
|
|
270
|
+
limit: int = 10,
|
|
271
|
+
) -> str:
|
|
272
|
+
"""Check the SAM.gov exclusions list (debarred / suspended entities).
|
|
273
|
+
|
|
274
|
+
Use this before subcontracting or teaming. Provide at least one of:
|
|
275
|
+
- name: company or individual name (partial match)
|
|
276
|
+
- uei: Unique Entity ID
|
|
277
|
+
- cage_code: CAGE code
|
|
278
|
+
|
|
279
|
+
Returns matching exclusion records with the reason, agency, and dates.
|
|
280
|
+
"""
|
|
281
|
+
if not (name or uei or cage_code):
|
|
282
|
+
raise RuntimeError("Provide at least one of: name, uei, cage_code")
|
|
283
|
+
params = {"name": name, "uei": uei, "cage_code": cage_code, "limit": limit}
|
|
284
|
+
data = await _get("/api/v1/exclusions/search", params)
|
|
285
|
+
return json.dumps(data, indent=2, default=str)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@mcp.tool()
|
|
289
|
+
async def search_awards(
|
|
290
|
+
awardee: Optional[str] = None,
|
|
291
|
+
uei: Optional[str] = None,
|
|
292
|
+
naics: Optional[str] = None,
|
|
293
|
+
agency: Optional[str] = None,
|
|
294
|
+
value_min: Optional[float] = None,
|
|
295
|
+
value_max: Optional[float] = None,
|
|
296
|
+
date_from: Optional[str] = None,
|
|
297
|
+
date_to: Optional[str] = None,
|
|
298
|
+
limit: int = 20,
|
|
299
|
+
offset: int = 0,
|
|
300
|
+
) -> str:
|
|
301
|
+
"""Search federal contract awards (who won, how much, when).
|
|
302
|
+
|
|
303
|
+
- awardee: company name (partial match)
|
|
304
|
+
- uei: Unique Entity ID
|
|
305
|
+
- naics: 6-digit NAICS code
|
|
306
|
+
- agency: agency name substring
|
|
307
|
+
- value_min / value_max: USD
|
|
308
|
+
- date_from / date_to: YYYY-MM-DD
|
|
309
|
+
- limit: max 1000
|
|
310
|
+
"""
|
|
311
|
+
params = {
|
|
312
|
+
"awardee": awardee, "uei": uei, "naics": naics, "agency": agency,
|
|
313
|
+
"value_min": value_min, "value_max": value_max,
|
|
314
|
+
"date_from": date_from, "date_to": date_to,
|
|
315
|
+
"limit": limit, "offset": offset,
|
|
316
|
+
}
|
|
317
|
+
data = await _get("/api/v1/awards/search", params)
|
|
318
|
+
return json.dumps(data, indent=2, default=str)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def main() -> None:
|
|
322
|
+
"""Entry point — runs the MCP server over stdio."""
|
|
323
|
+
mcp.run()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "govconapi-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for GovCon API – federal contract opportunities, awards, and exclusions in Claude/ChatGPT/any MCP client"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "GovCon API", email = "support@govconapi.com" }]
|
|
13
|
+
keywords = ["mcp", "model-context-protocol", "federal-contracts", "sam-gov", "govcon", "claude"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Software Development :: Libraries",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"mcp>=1.0.0",
|
|
26
|
+
"httpx>=0.27.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://govconapi.com"
|
|
31
|
+
Documentation = "https://govconapi.com/api-guide"
|
|
32
|
+
Repository = "https://github.com/rozetyp/sam-api"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
govconapi-mcp = "govconapi_mcp.server:main"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["govconapi_mcp"]
|