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.
@@ -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,2 @@
1
+ """ListRiver MCP server. https://github.com/ListRiver-LLC/listriver-api/tree/main/mcp"""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .server import main
2
+
3
+ main()
@@ -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,2 @@
1
+ [console_scripts]
2
+ listriver-mcp = listriver_mcp.server:main
@@ -0,0 +1,2 @@
1
+ mcp<3,>=2.3
2
+ listriver>=0.1.0
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"]