listriver-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.
- listriver_mcp-0.1.0/PKG-INFO +83 -0
- listriver_mcp-0.1.0/README.md +63 -0
- listriver_mcp-0.1.0/listriver_mcp/__init__.py +2 -0
- listriver_mcp-0.1.0/listriver_mcp/__main__.py +3 -0
- listriver_mcp-0.1.0/listriver_mcp/server.py +186 -0
- listriver_mcp-0.1.0/listriver_mcp.egg-info/PKG-INFO +83 -0
- listriver_mcp-0.1.0/listriver_mcp.egg-info/SOURCES.txt +12 -0
- listriver_mcp-0.1.0/listriver_mcp.egg-info/dependency_links.txt +1 -0
- listriver_mcp-0.1.0/listriver_mcp.egg-info/entry_points.txt +2 -0
- listriver_mcp-0.1.0/listriver_mcp.egg-info/requires.txt +2 -0
- listriver_mcp-0.1.0/listriver_mcp.egg-info/top_level.txt +1 -0
- listriver_mcp-0.1.0/pyproject.toml +32 -0
- listriver_mcp-0.1.0/setup.cfg +4 -0
- listriver_mcp-0.1.0/tests/test_server.py +78 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: listriver-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for ListRiver: let Claude and other AI assistants search new U.S. businesses from state registries.
|
|
5
|
+
Author-email: ListRiver LLC <support@listriver.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://listriver.com/api-docs
|
|
8
|
+
Project-URL: Source, https://github.com/ListRiver-LLC/listriver-api
|
|
9
|
+
Project-URL: Get a free API key, https://listriver.com/Account/ApiSignUp.aspx
|
|
10
|
+
Keywords: mcp,model context protocol,claude,business data,secretary of state,new businesses,b2b leads,kyb
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Topic :: Office/Business
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: mcp<3,>=2.3
|
|
19
|
+
Requires-Dist: listriver>=0.1.0
|
|
20
|
+
|
|
21
|
+
# ListRiver MCP server
|
|
22
|
+
|
|
23
|
+
<!-- mcp-name: io.github.ListRiver-LLC/listriver -->
|
|
24
|
+
|
|
25
|
+
Lets Claude and other AI assistants search **new and active U.S. businesses from official state business registries**
|
|
26
|
+
through [ListRiver](https://listriver.com/api-docs): new LLCs, corporations and nonprofits by state, city, county, ZIP,
|
|
27
|
+
NAICS industry, entity type, formation month, and whether a phone or email is on file.
|
|
28
|
+
|
|
29
|
+
Ask things like:
|
|
30
|
+
|
|
31
|
+
- "How many businesses were formed in Miami-Dade in the last 3 months that have a phone number?"
|
|
32
|
+
- "Find 20 new construction companies in Harris County, Texas."
|
|
33
|
+
- "List new LLCs in ZIP 80202 formed this month, with their registered agents."
|
|
34
|
+
|
|
35
|
+
## Tools
|
|
36
|
+
|
|
37
|
+
| Tool | What it does | Cost |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `list_states` | States covered and their record counts | free |
|
|
40
|
+
| `count_businesses` | How many businesses match a search, per state | free |
|
|
41
|
+
| `search_businesses` | The matching businesses (25 by default, up to 100 per call; `cursor` for more) | each business returned counts against your ListRiver records |
|
|
42
|
+
|
|
43
|
+
The assistant is told to count first and to ask only for as many businesses as you need.
|
|
44
|
+
|
|
45
|
+
## Setup
|
|
46
|
+
|
|
47
|
+
1. Get a free API key (500 records a month): [listriver.com/Account/ApiSignUp.aspx](https://listriver.com/Account/ApiSignUp.aspx)
|
|
48
|
+
2. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) (it runs the server with `uvx`, no other install needed).
|
|
49
|
+
3. Add the server to your AI app.
|
|
50
|
+
|
|
51
|
+
**Claude Desktop** (Settings > Developer > Edit Config, then restart Claude Desktop):
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"listriver": {
|
|
57
|
+
"command": "uvx",
|
|
58
|
+
"args": ["listriver-mcp"],
|
|
59
|
+
"env": { "LISTRIVER_API_KEY": "lr_key_..." }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Claude Code:**
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
claude mcp add listriver -e LISTRIVER_API_KEY=lr_key_... -- uvx listriver-mcp
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Other MCP clients:** run `uvx listriver-mcp` (stdio) with `LISTRIVER_API_KEY` in the environment. Or `pip install listriver-mcp`
|
|
72
|
+
and run `listriver-mcp`.
|
|
73
|
+
|
|
74
|
+
## Notes
|
|
75
|
+
|
|
76
|
+
- Every business returned counts against your records each time it is returned; repeated or overlapping searches count
|
|
77
|
+
again. Counting is free.
|
|
78
|
+
- Phone, email, NAICS and county are on file for only part of the records, and coverage varies by state.
|
|
79
|
+
- The data is for your own business use, not for resale or for credit, insurance, employment or housing decisions
|
|
80
|
+
([ListRiver API agreement](https://listriver.com/legal/listriver-api-eula.txt)).
|
|
81
|
+
- More: [API documentation](https://listriver.com/api-docs) - [support@listriver.com](mailto:support@listriver.com)
|
|
82
|
+
|
|
83
|
+
MIT license.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# ListRiver MCP server
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.ListRiver-LLC/listriver -->
|
|
4
|
+
|
|
5
|
+
Lets Claude and other AI assistants search **new and active U.S. businesses from official state business registries**
|
|
6
|
+
through [ListRiver](https://listriver.com/api-docs): new LLCs, corporations and nonprofits by state, city, county, ZIP,
|
|
7
|
+
NAICS industry, entity type, formation month, and whether a phone or email is on file.
|
|
8
|
+
|
|
9
|
+
Ask things like:
|
|
10
|
+
|
|
11
|
+
- "How many businesses were formed in Miami-Dade in the last 3 months that have a phone number?"
|
|
12
|
+
- "Find 20 new construction companies in Harris County, Texas."
|
|
13
|
+
- "List new LLCs in ZIP 80202 formed this month, with their registered agents."
|
|
14
|
+
|
|
15
|
+
## Tools
|
|
16
|
+
|
|
17
|
+
| Tool | What it does | Cost |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `list_states` | States covered and their record counts | free |
|
|
20
|
+
| `count_businesses` | How many businesses match a search, per state | free |
|
|
21
|
+
| `search_businesses` | The matching businesses (25 by default, up to 100 per call; `cursor` for more) | each business returned counts against your ListRiver records |
|
|
22
|
+
|
|
23
|
+
The assistant is told to count first and to ask only for as many businesses as you need.
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
1. Get a free API key (500 records a month): [listriver.com/Account/ApiSignUp.aspx](https://listriver.com/Account/ApiSignUp.aspx)
|
|
28
|
+
2. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) (it runs the server with `uvx`, no other install needed).
|
|
29
|
+
3. Add the server to your AI app.
|
|
30
|
+
|
|
31
|
+
**Claude Desktop** (Settings > Developer > Edit Config, then restart Claude Desktop):
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"listriver": {
|
|
37
|
+
"command": "uvx",
|
|
38
|
+
"args": ["listriver-mcp"],
|
|
39
|
+
"env": { "LISTRIVER_API_KEY": "lr_key_..." }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Claude Code:**
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
claude mcp add listriver -e LISTRIVER_API_KEY=lr_key_... -- uvx listriver-mcp
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Other MCP clients:** run `uvx listriver-mcp` (stdio) with `LISTRIVER_API_KEY` in the environment. Or `pip install listriver-mcp`
|
|
52
|
+
and run `listriver-mcp`.
|
|
53
|
+
|
|
54
|
+
## Notes
|
|
55
|
+
|
|
56
|
+
- Every business returned counts against your records each time it is returned; repeated or overlapping searches count
|
|
57
|
+
again. Counting is free.
|
|
58
|
+
- Phone, email, NAICS and county are on file for only part of the records, and coverage varies by state.
|
|
59
|
+
- The data is for your own business use, not for resale or for credit, insurance, employment or housing decisions
|
|
60
|
+
([ListRiver API agreement](https://listriver.com/legal/listriver-api-eula.txt)).
|
|
61
|
+
- More: [API documentation](https://listriver.com/api-docs) - [support@listriver.com](mailto:support@listriver.com)
|
|
62
|
+
|
|
63
|
+
MIT license.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""ListRiver MCP server: lets AI assistants search new and active U.S. businesses from state registries.
|
|
2
|
+
|
|
3
|
+
Run with `uvx listriver-mcp` and set LISTRIVER_API_KEY (free key: https://listriver.com/Account/ApiSignUp.aspx).
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from listriver import AuthError, BadRequest, Client, ListRiverError, OutOfRecords
|
|
11
|
+
from mcp.server.mcpserver import MCPServer
|
|
12
|
+
from mcp.types import ToolAnnotations
|
|
13
|
+
|
|
14
|
+
KEY_URL = "https://listriver.com/Account/ApiSignUp.aspx"
|
|
15
|
+
MAX_SEARCH = 100
|
|
16
|
+
|
|
17
|
+
INSTRUCTIONS = f"""ListRiver has active U.S. businesses from official state business registries (new LLCs, corporations,
|
|
18
|
+
nonprofits, partnerships), with address, county, registered agent, formation month and, where available, phone, email,
|
|
19
|
+
website, EIN and NAICS industry.
|
|
20
|
+
|
|
21
|
+
How to use these tools well:
|
|
22
|
+
- count_businesses is free. Call it first to see how many businesses match before searching.
|
|
23
|
+
- search_businesses uses the user's ListRiver records: every business returned counts, each time it is returned.
|
|
24
|
+
Ask for only as many as the user needs (default 25, at most {MAX_SEARCH} per call), and don't repeat the same search.
|
|
25
|
+
To get more of the same search, pass the cursor from the previous answer.
|
|
26
|
+
- city, county and zip need states. Formation dates are month-level (formed_within counts whole calendar months).
|
|
27
|
+
- Phone, email, NAICS and county are on file for only part of the records, and coverage varies by state:
|
|
28
|
+
use has_phone / has_email / naics with count_businesses to check before promising them.
|
|
29
|
+
- Data is for the user's own business use; it may not be used for credit, insurance, employment or housing decisions."""
|
|
30
|
+
|
|
31
|
+
READ_ONLY = ToolAnnotations(readOnlyHint=True, idempotentHint=True, openWorldHint=True)
|
|
32
|
+
|
|
33
|
+
server = MCPServer(
|
|
34
|
+
name="ListRiver",
|
|
35
|
+
title="ListRiver business registry data",
|
|
36
|
+
instructions=INSTRUCTIONS,
|
|
37
|
+
website_url="https://listriver.com/api-docs",
|
|
38
|
+
version="0.1.0",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_client: Optional[Client] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_client() -> Client:
|
|
45
|
+
global _client
|
|
46
|
+
if _client is None:
|
|
47
|
+
key = (os.environ.get("LISTRIVER_API_KEY") or "").strip()
|
|
48
|
+
if not key:
|
|
49
|
+
raise ListRiverError(401, f"LISTRIVER_API_KEY is not set. Get a free key at {KEY_URL} and add it to this MCP server's environment.")
|
|
50
|
+
_client = Client(key)
|
|
51
|
+
return _client
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _filters(states, city, county, zip, naics, entity_type, has_phone, has_email, formed_within, formed_from, formed_to) -> Dict[str, Any]:
|
|
55
|
+
return dict(states=states, city=city, county=county, zip=zip, naics=naics, entity_type=entity_type,
|
|
56
|
+
has_phone=has_phone, has_email=has_email, formed_within=formed_within,
|
|
57
|
+
formed_from=formed_from, formed_to=formed_to)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _error(e: ListRiverError) -> Dict[str, Any]:
|
|
61
|
+
hint = None
|
|
62
|
+
if isinstance(e, AuthError):
|
|
63
|
+
hint = f"Check LISTRIVER_API_KEY. A free key is at {KEY_URL}."
|
|
64
|
+
elif isinstance(e, OutOfRecords):
|
|
65
|
+
hint = "The user's records are used up. They can buy a pack or plan at https://listriver.com/Account/Api.aspx (count_businesses is still free)."
|
|
66
|
+
elif isinstance(e, BadRequest):
|
|
67
|
+
hint = "Fix the parameter the message names and try again."
|
|
68
|
+
return {"ok": False, "status": e.status, "error": e.message, "hint": hint}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@server.tool(annotations=READ_ONLY)
|
|
72
|
+
def list_states() -> Dict[str, Any]:
|
|
73
|
+
"""List the states ListRiver covers, with how many businesses each has: formed this month or last ("recent"),
|
|
74
|
+
in the last 12 months ("year"), and every active business ("all"). Free."""
|
|
75
|
+
try:
|
|
76
|
+
return {"ok": True, "states": _get_client().states()}
|
|
77
|
+
except ListRiverError as e:
|
|
78
|
+
return _error(e)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@server.tool(annotations=READ_ONLY)
|
|
82
|
+
def count_businesses(
|
|
83
|
+
states: Optional[str] = None,
|
|
84
|
+
city: Optional[str] = None,
|
|
85
|
+
county: Optional[str] = None,
|
|
86
|
+
zip: Optional[str] = None,
|
|
87
|
+
naics: Optional[str] = None,
|
|
88
|
+
entity_type: Optional[str] = None,
|
|
89
|
+
has_phone: bool = False,
|
|
90
|
+
has_email: bool = False,
|
|
91
|
+
formed_within: Optional[int] = 1,
|
|
92
|
+
formed_from: Optional[str] = None,
|
|
93
|
+
formed_to: Optional[str] = None,
|
|
94
|
+
) -> Dict[str, Any]:
|
|
95
|
+
"""Count businesses matching the filters, per state. Free: use it before search_businesses.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
states: Comma-separated two-letter registration states, e.g. "FL" or "FL,TX". Required with city, county or zip.
|
|
99
|
+
city: Comma-separated business cities, e.g. "MIAMI".
|
|
100
|
+
county: Comma-separated counties, e.g. "MIAMI-DADE" (or "FL:MIAMI-DADE").
|
|
101
|
+
zip: Comma-separated 5-digit ZIP codes or 3-digit ZIP prefixes, e.g. "33139,331".
|
|
102
|
+
naics: Comma-separated NAICS prefixes, 2-6 digits, e.g. "23" (construction) or "722511".
|
|
103
|
+
entity_type: Comma-separated of LLC, CORP, LP, LLP, NONPROFIT, OTHER.
|
|
104
|
+
has_phone: Only businesses with a phone number on file.
|
|
105
|
+
has_email: Only businesses with an email on file.
|
|
106
|
+
formed_within: Formed in the current month or the N months before it (1 = this month or last). Set 0 for any formation date.
|
|
107
|
+
formed_from: First formation month, "YYYY-MM" (overrides formed_within).
|
|
108
|
+
formed_to: Last formation month, "YYYY-MM" (overrides formed_within).
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
f = _filters(states, city, county, zip, naics, entity_type, has_phone, has_email,
|
|
112
|
+
_window(formed_within, formed_from, formed_to), formed_from, formed_to)
|
|
113
|
+
if f["formed_within"] is None and not formed_from and not formed_to:
|
|
114
|
+
f["preset"] = "all"
|
|
115
|
+
return {"ok": True, **_get_client().count(**f)}
|
|
116
|
+
except ListRiverError as e:
|
|
117
|
+
return _error(e)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@server.tool(annotations=READ_ONLY)
|
|
121
|
+
def search_businesses(
|
|
122
|
+
states: Optional[str] = None,
|
|
123
|
+
city: Optional[str] = None,
|
|
124
|
+
county: Optional[str] = None,
|
|
125
|
+
zip: Optional[str] = None,
|
|
126
|
+
naics: Optional[str] = None,
|
|
127
|
+
entity_type: Optional[str] = None,
|
|
128
|
+
has_phone: bool = False,
|
|
129
|
+
has_email: bool = False,
|
|
130
|
+
formed_within: Optional[int] = 1,
|
|
131
|
+
formed_from: Optional[str] = None,
|
|
132
|
+
formed_to: Optional[str] = None,
|
|
133
|
+
limit: int = 25,
|
|
134
|
+
cursor: Optional[str] = None,
|
|
135
|
+
) -> Dict[str, Any]:
|
|
136
|
+
"""Get businesses matching the filters (same filters as count_businesses). USES THE USER'S RECORDS: each business
|
|
137
|
+
returned counts, every time it is returned, so ask only for what the user needs.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
states: Comma-separated two-letter registration states. Required with city, county or zip.
|
|
141
|
+
city: Comma-separated business cities.
|
|
142
|
+
county: Comma-separated counties.
|
|
143
|
+
zip: Comma-separated 5-digit ZIP codes or 3-digit prefixes.
|
|
144
|
+
naics: Comma-separated NAICS prefixes, 2-6 digits.
|
|
145
|
+
entity_type: Comma-separated of LLC, CORP, LP, LLP, NONPROFIT, OTHER.
|
|
146
|
+
has_phone: Only businesses with a phone number on file.
|
|
147
|
+
has_email: Only businesses with an email on file.
|
|
148
|
+
formed_within: Formed in the current month or the N months before it (1 = this month or last; 0 = any date).
|
|
149
|
+
formed_from: First formation month, "YYYY-MM".
|
|
150
|
+
formed_to: Last formation month, "YYYY-MM".
|
|
151
|
+
limit: How many businesses to return, 1-100 (default 25).
|
|
152
|
+
cursor: The cursor from a previous search_businesses answer, to get the next businesses of the same search.
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
f = _filters(states, city, county, zip, naics, entity_type, has_phone, has_email,
|
|
156
|
+
_window(formed_within, formed_from, formed_to), formed_from, formed_to)
|
|
157
|
+
if f["formed_within"] is None and not formed_from and not formed_to:
|
|
158
|
+
f["preset"] = "all"
|
|
159
|
+
page = _get_client().page(limit=max(1, min(MAX_SEARCH, int(limit))), cursor=cursor, **f)
|
|
160
|
+
return {
|
|
161
|
+
"ok": True,
|
|
162
|
+
"count": len(page.rows),
|
|
163
|
+
"businesses": page.rows,
|
|
164
|
+
"cursor": page.cursor,
|
|
165
|
+
"more": page.has_more,
|
|
166
|
+
"records_left_this_period": page.monthly_remaining,
|
|
167
|
+
"pack_records_left": page.credits_remaining,
|
|
168
|
+
}
|
|
169
|
+
except ListRiverError as e:
|
|
170
|
+
return _error(e)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _window(formed_within: Optional[int], formed_from: Optional[str], formed_to: Optional[str]) -> Optional[int]:
|
|
174
|
+
if formed_from or formed_to:
|
|
175
|
+
return None
|
|
176
|
+
if formed_within is None or int(formed_within) <= 0:
|
|
177
|
+
return None
|
|
178
|
+
return int(formed_within)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main() -> None:
|
|
182
|
+
server.run("stdio")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
main()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: listriver-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for ListRiver: let Claude and other AI assistants search new U.S. businesses from state registries.
|
|
5
|
+
Author-email: ListRiver LLC <support@listriver.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://listriver.com/api-docs
|
|
8
|
+
Project-URL: Source, https://github.com/ListRiver-LLC/listriver-api
|
|
9
|
+
Project-URL: Get a free API key, https://listriver.com/Account/ApiSignUp.aspx
|
|
10
|
+
Keywords: mcp,model context protocol,claude,business data,secretary of state,new businesses,b2b leads,kyb
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Topic :: Office/Business
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: mcp<3,>=2.3
|
|
19
|
+
Requires-Dist: listriver>=0.1.0
|
|
20
|
+
|
|
21
|
+
# ListRiver MCP server
|
|
22
|
+
|
|
23
|
+
<!-- mcp-name: io.github.ListRiver-LLC/listriver -->
|
|
24
|
+
|
|
25
|
+
Lets Claude and other AI assistants search **new and active U.S. businesses from official state business registries**
|
|
26
|
+
through [ListRiver](https://listriver.com/api-docs): new LLCs, corporations and nonprofits by state, city, county, ZIP,
|
|
27
|
+
NAICS industry, entity type, formation month, and whether a phone or email is on file.
|
|
28
|
+
|
|
29
|
+
Ask things like:
|
|
30
|
+
|
|
31
|
+
- "How many businesses were formed in Miami-Dade in the last 3 months that have a phone number?"
|
|
32
|
+
- "Find 20 new construction companies in Harris County, Texas."
|
|
33
|
+
- "List new LLCs in ZIP 80202 formed this month, with their registered agents."
|
|
34
|
+
|
|
35
|
+
## Tools
|
|
36
|
+
|
|
37
|
+
| Tool | What it does | Cost |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `list_states` | States covered and their record counts | free |
|
|
40
|
+
| `count_businesses` | How many businesses match a search, per state | free |
|
|
41
|
+
| `search_businesses` | The matching businesses (25 by default, up to 100 per call; `cursor` for more) | each business returned counts against your ListRiver records |
|
|
42
|
+
|
|
43
|
+
The assistant is told to count first and to ask only for as many businesses as you need.
|
|
44
|
+
|
|
45
|
+
## Setup
|
|
46
|
+
|
|
47
|
+
1. Get a free API key (500 records a month): [listriver.com/Account/ApiSignUp.aspx](https://listriver.com/Account/ApiSignUp.aspx)
|
|
48
|
+
2. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) (it runs the server with `uvx`, no other install needed).
|
|
49
|
+
3. Add the server to your AI app.
|
|
50
|
+
|
|
51
|
+
**Claude Desktop** (Settings > Developer > Edit Config, then restart Claude Desktop):
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"listriver": {
|
|
57
|
+
"command": "uvx",
|
|
58
|
+
"args": ["listriver-mcp"],
|
|
59
|
+
"env": { "LISTRIVER_API_KEY": "lr_key_..." }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Claude Code:**
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
claude mcp add listriver -e LISTRIVER_API_KEY=lr_key_... -- uvx listriver-mcp
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Other MCP clients:** run `uvx listriver-mcp` (stdio) with `LISTRIVER_API_KEY` in the environment. Or `pip install listriver-mcp`
|
|
72
|
+
and run `listriver-mcp`.
|
|
73
|
+
|
|
74
|
+
## Notes
|
|
75
|
+
|
|
76
|
+
- Every business returned counts against your records each time it is returned; repeated or overlapping searches count
|
|
77
|
+
again. Counting is free.
|
|
78
|
+
- Phone, email, NAICS and county are on file for only part of the records, and coverage varies by state.
|
|
79
|
+
- The data is for your own business use, not for resale or for credit, insurance, employment or housing decisions
|
|
80
|
+
([ListRiver API agreement](https://listriver.com/legal/listriver-api-eula.txt)).
|
|
81
|
+
- More: [API documentation](https://listriver.com/api-docs) - [support@listriver.com](mailto:support@listriver.com)
|
|
82
|
+
|
|
83
|
+
MIT license.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
listriver_mcp/__init__.py
|
|
4
|
+
listriver_mcp/__main__.py
|
|
5
|
+
listriver_mcp/server.py
|
|
6
|
+
listriver_mcp.egg-info/PKG-INFO
|
|
7
|
+
listriver_mcp.egg-info/SOURCES.txt
|
|
8
|
+
listriver_mcp.egg-info/dependency_links.txt
|
|
9
|
+
listriver_mcp.egg-info/entry_points.txt
|
|
10
|
+
listriver_mcp.egg-info/requires.txt
|
|
11
|
+
listriver_mcp.egg-info/top_level.txt
|
|
12
|
+
tests/test_server.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
listriver_mcp
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "listriver-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for ListRiver: let Claude and other AI assistants search new U.S. businesses from state registries."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "ListRiver LLC", email = "support@listriver.com" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = ["mcp>=2.3,<3", "listriver>=0.1.0"]
|
|
14
|
+
keywords = ["mcp", "model context protocol", "claude", "business data", "secretary of state", "new businesses", "b2b leads", "kyb"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Topic :: Office/Business",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
listriver-mcp = "listriver_mcp.server:main"
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://listriver.com/api-docs"
|
|
28
|
+
Source = "https://github.com/ListRiver-LLC/listriver-api"
|
|
29
|
+
"Get a free API key" = "https://listriver.com/Account/ApiSignUp.aspx"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools]
|
|
32
|
+
packages = ["listriver_mcp"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from listriver import OutOfRecords, Page
|
|
7
|
+
import listriver_mcp.server as srv
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FakeClient:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.calls = []
|
|
13
|
+
|
|
14
|
+
def states(self):
|
|
15
|
+
self.calls.append(("states", {}))
|
|
16
|
+
return [{"code": "FL", "name": "Florida", "recent": 10, "year": 100, "all": 1000}]
|
|
17
|
+
|
|
18
|
+
def count(self, **f):
|
|
19
|
+
self.calls.append(("count", f))
|
|
20
|
+
return {"total": 7, "states": [{"code": "FL", "count": 7}], "approximate": True}
|
|
21
|
+
|
|
22
|
+
def page(self, limit, cursor=None, **f):
|
|
23
|
+
self.calls.append(("page", dict(f, limit=limit, cursor=cursor)))
|
|
24
|
+
return Page(rows=[{"businessName": "A LLC"}], cursor="FL.5", resume="FL.5", monthly_remaining=499, credits_remaining=0)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def fake(monkeypatch):
|
|
29
|
+
c = FakeClient()
|
|
30
|
+
monkeypatch.setattr(srv, "_client", c)
|
|
31
|
+
return c
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def call(name, args):
|
|
35
|
+
result = asyncio.run(srv.server.call_tool(name, args))
|
|
36
|
+
text = result.content[0].text
|
|
37
|
+
return json.loads(text)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_tools_are_listed_and_read_only():
|
|
41
|
+
tools = {t.name: t for t in asyncio.run(srv.server.list_tools())}
|
|
42
|
+
assert set(tools) == {"list_states", "count_businesses", "search_businesses"}
|
|
43
|
+
assert all(t.annotations.read_only_hint for t in tools.values())
|
|
44
|
+
assert "USES THE USER'S RECORDS" in tools["search_businesses"].description
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_count_passes_filters(fake):
|
|
48
|
+
out = call("count_businesses", {"states": "FL", "city": "MIAMI", "has_phone": True, "formed_within": 3})
|
|
49
|
+
assert out["ok"] and out["total"] == 7
|
|
50
|
+
name, f = fake.calls[0]
|
|
51
|
+
assert name == "count" and f["states"] == "FL" and f["city"] == "MIAMI" and f["has_phone"] is True and f["formed_within"] == 3
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_any_formation_date_uses_preset_all(fake):
|
|
55
|
+
call("count_businesses", {"states": "FL", "formed_within": 0})
|
|
56
|
+
assert fake.calls[0][1]["preset"] == "all" and fake.calls[0][1]["formed_within"] is None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_search_caps_the_limit_and_returns_cursor(fake):
|
|
60
|
+
out = call("search_businesses", {"states": "FL", "limit": 5000, "cursor": "FL.1"})
|
|
61
|
+
assert out["count"] == 1 and out["cursor"] == "FL.5" and out["more"] is True and out["records_left_this_period"] == 499
|
|
62
|
+
assert fake.calls[0][1]["limit"] == 100 and fake.calls[0][1]["cursor"] == "FL.1"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_out_of_records_gives_a_hint(monkeypatch):
|
|
66
|
+
class Broke(FakeClient):
|
|
67
|
+
def page(self, limit, cursor=None, **f):
|
|
68
|
+
raise OutOfRecords(429, "Your free 500 records for this month are used up.")
|
|
69
|
+
monkeypatch.setattr(srv, "_client", Broke())
|
|
70
|
+
out = call("search_businesses", {"states": "FL"})
|
|
71
|
+
assert out["ok"] is False and out["status"] == 429 and "Account/Api.aspx" in out["hint"]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_missing_key_explains_how_to_get_one(monkeypatch):
|
|
75
|
+
monkeypatch.setattr(srv, "_client", None)
|
|
76
|
+
monkeypatch.delenv("LISTRIVER_API_KEY", raising=False)
|
|
77
|
+
out = call("list_states", {})
|
|
78
|
+
assert out["ok"] is False and "ApiSignUp.aspx" in out["error"]
|