Skip to content

Commit 7d18d3b

Browse files
dschwarz26claude
andauthored
Implement built in list browsing and fetching (#241)
* Add built-in lists: SDK functions and MCP tools Adds list_built_in_datasets() and use_built_in_list() SDK functions, plus everyrow_browse_lists and everyrow_use_list MCP tools. use_list copies the dataset into a session, fetches the data, and saves it as a CSV file ready to pass to screen/rank/agent/dedupe operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Improve reference list tool descriptions for self-discoverability Rename "built-in" to "reference lists" throughout. Expand browse_lists docstring to enumerate what's available (stock indices, sector breakdowns, countries, people, institutions, infrastructure) so agents know when to use it without needing a separate system prompt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add browse_lists and use_list tools to manifest.json Fixes test_manifest_sync test failure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add new tools to test_list_tools expected tool list Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4d1131f commit 7d18d3b

5 files changed

Lines changed: 261 additions & 0 deletions

File tree

everyrow-mcp/manifest.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@
8080
{
8181
"name": "everyrow_balance",
8282
"description": "Check the current billing balance for the authenticated user."
83+
},
84+
{
85+
"name": "everyrow_browse_lists",
86+
"description": "Browse available reference lists of well-known entities."
87+
},
88+
{
89+
"name": "everyrow_use_list",
90+
"description": "Import a reference list into your session and save it as a CSV file."
8391
}
8492
],
8593
"user_config": {

everyrow-mcp/src/everyrow_mcp/models.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,32 @@ def _validate_task_id(v: str) -> str:
569569
return v
570570

571571

572+
class BrowseListsInput(BaseModel):
573+
"""Input for browsing reference lists."""
574+
575+
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
576+
577+
search: str | None = Field(
578+
default=None,
579+
description="Search term to match against list names (case-insensitive).",
580+
)
581+
category: str | None = Field(
582+
default=None,
583+
description="Filter by category (e.g. 'Finance', 'Geography').",
584+
)
585+
586+
587+
class UseListInput(BaseModel):
588+
"""Input for importing a reference list into a session."""
589+
590+
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
591+
592+
artifact_id: str = Field(
593+
...,
594+
description="artifact_id from everyrow_browse_lists results.",
595+
)
596+
597+
572598
class ProgressInput(BaseModel):
573599
"""Input for checking task progress."""
574600

everyrow-mcp/src/everyrow_mcp/tools.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import pandas as pd
1212
from everyrow.api_utils import handle_response
13+
from everyrow.built_in_lists import list_built_in_datasets, use_built_in_list
1314
from everyrow.constants import EveryrowError
1415
from everyrow.generated.api.billing import get_billing_balance_billing_get
1516
from everyrow.generated.api.tasks import get_task_status_tasks_task_id_status_get
@@ -35,6 +36,7 @@
3536
from everyrow_mcp.config import settings
3637
from everyrow_mcp.models import (
3738
AgentInput,
39+
BrowseListsInput,
3840
CancelInput,
3941
DedupeInput,
4042
ForecastInput,
@@ -47,6 +49,7 @@
4749
SingleAgentInput,
4850
StdioResultsInput,
4951
UploadDataInput,
52+
UseListInput,
5053
_schema_to_model,
5154
)
5255
from everyrow_mcp.result_store import (
@@ -102,6 +105,124 @@ async def _check_task_ownership(task_id: str) -> list[TextContent] | None:
102105
return None
103106

104107

108+
@mcp.tool(
109+
name="everyrow_browse_lists",
110+
structured_output=False,
111+
annotations=ToolAnnotations(
112+
title="Browse Reference Lists",
113+
readOnlyHint=True,
114+
destructiveHint=False,
115+
idempotentHint=True,
116+
openWorldHint=False,
117+
),
118+
)
119+
async def everyrow_browse_lists(
120+
params: BrowseListsInput, ctx: EveryRowContext
121+
) -> list[TextContent]:
122+
"""Browse available reference lists of well-known entities.
123+
124+
Includes company lists (S&P 500, FTSE 100, Russell 3000, sector breakdowns
125+
like Global Banks or Semiconductor companies), geographic lists (all countries,
126+
EU members, US states, major cities), people (billionaires, heads of state,
127+
AI leaders), institutions (top universities, regulators), and infrastructure
128+
(airports, ports, power stations).
129+
130+
Use this when the user's analysis involves a well-known group that we might
131+
already have a list for. Returns names, fields, and artifact_ids to pass to
132+
everyrow_use_list.
133+
134+
Call with no parameters to see all available lists, or use search/category
135+
to narrow results.
136+
"""
137+
client = _get_client(ctx)
138+
139+
try:
140+
results = await list_built_in_datasets(
141+
client, search=params.search, category=params.category
142+
)
143+
except Exception as e:
144+
return [TextContent(type="text", text=f"Error browsing built-in lists: {e!r}")]
145+
146+
if not results:
147+
search_desc = f" matching '{params.search}'" if params.search else ""
148+
cat_desc = f" in category '{params.category}'" if params.category else ""
149+
return [
150+
TextContent(
151+
type="text",
152+
text=f"No built-in lists found{search_desc}{cat_desc}.",
153+
)
154+
]
155+
156+
lines = [f"Found {len(results)} built-in list(s):\n"]
157+
for i, item in enumerate(results, 1):
158+
fields_str = ", ".join(item.fields) if item.fields else "(no fields listed)"
159+
lines.append(
160+
f"{i}. {item.name} [{item.category}]\n"
161+
f" Fields: {fields_str}\n"
162+
f" artifact_id: {item.artifact_id}\n"
163+
)
164+
lines.append(
165+
"To use one of these lists, call everyrow_use_list with the artifact_id."
166+
)
167+
168+
return [TextContent(type="text", text="\n".join(lines))]
169+
170+
171+
@mcp.tool(
172+
name="everyrow_use_list",
173+
structured_output=False,
174+
annotations=ToolAnnotations(
175+
title="Import Reference List",
176+
readOnlyHint=False,
177+
destructiveHint=False,
178+
idempotentHint=False,
179+
openWorldHint=False,
180+
),
181+
)
182+
async def everyrow_use_list(
183+
params: UseListInput, ctx: EveryRowContext
184+
) -> list[TextContent]:
185+
"""Import a reference list into your session and save it as a CSV file.
186+
187+
This copies the dataset into a new session, fetches the data, and saves
188+
it as a CSV file ready to pass to other everyrow utilities for analysis
189+
or research.
190+
191+
The copy is a fast database operation (<1s) — no polling needed.
192+
"""
193+
client = _get_client(ctx)
194+
195+
try:
196+
async with create_session(client=client) as session:
197+
session_url = session.get_url()
198+
result = await use_built_in_list(
199+
artifact_id=UUID(params.artifact_id),
200+
session=session,
201+
)
202+
203+
# Fetch the copied data and save as CSV
204+
df, _ = await _fetch_task_result(client, str(result.task_id))
205+
206+
csv_path = Path.cwd() / f"built-in-list-{result.artifact_id}.csv"
207+
df.to_csv(csv_path, index=False)
208+
except Exception as e:
209+
return [TextContent(type="text", text=f"Error importing built-in list: {e!r}")]
210+
211+
return [
212+
TextContent(
213+
type="text",
214+
text=(
215+
f"Imported built-in list into your session.\n\n"
216+
f"CSV saved to: {csv_path}\n"
217+
f"Rows: {len(df)}\n"
218+
f"Columns: {', '.join(df.columns)}\n"
219+
f"Session: {session_url}\n\n"
220+
f"Pass {csv_path} as input_csv to other everyrow utilities for analysis or research."
221+
),
222+
)
223+
]
224+
225+
105226
@mcp.tool(
106227
name="everyrow_agent",
107228
structured_output=False,

everyrow-mcp/tests/test_mcp_e2e.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ async def test_list_tools(self, _http_state):
174174
[
175175
"everyrow_agent",
176176
"everyrow_balance",
177+
"everyrow_browse_lists",
177178
"everyrow_cancel",
178179
"everyrow_dedupe",
179180
"everyrow_forecast",
@@ -185,6 +186,7 @@ async def test_list_tools(self, _http_state):
185186
"everyrow_screen",
186187
"everyrow_single_agent",
187188
"everyrow_upload_data",
189+
"everyrow_use_list",
188190
]
189191
)
190192
assert tool_names == expected

src/everyrow/built_in_lists.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Built-in lists: browse and import pre-built datasets."""
2+
3+
from dataclasses import dataclass
4+
from uuid import UUID
5+
6+
from everyrow.constants import EveryrowError
7+
from everyrow.generated.client import AuthenticatedClient
8+
from everyrow.session import Session
9+
10+
11+
@dataclass
12+
class BuiltInListItem:
13+
"""A built-in dataset available for import."""
14+
15+
name: str
16+
artifact_id: UUID
17+
category: str
18+
fields: list[str]
19+
20+
21+
@dataclass
22+
class UseBuiltInListResult:
23+
"""Result of importing a built-in list into a session."""
24+
25+
artifact_id: UUID
26+
session_id: UUID
27+
task_id: UUID
28+
29+
30+
async def list_built_in_datasets(
31+
client: AuthenticatedClient,
32+
search: str | None = None,
33+
category: str | None = None,
34+
) -> list[BuiltInListItem]:
35+
"""Fetch available built-in datasets from the API.
36+
37+
Args:
38+
client: Authenticated API client.
39+
search: Optional search term to match against list names (case-insensitive).
40+
category: Optional category filter.
41+
42+
Returns:
43+
List of available built-in datasets.
44+
"""
45+
params: dict[str, str] = {}
46+
if search:
47+
params["search"] = search
48+
if category:
49+
params["category"] = category
50+
51+
response = await client.get_async_httpx_client().request(
52+
method="GET",
53+
url="/built-in-lists",
54+
params=params,
55+
)
56+
if response.status_code != 200:
57+
raise EveryrowError(f"Failed to list built-in datasets: {response.text}")
58+
59+
data = response.json()
60+
return [
61+
BuiltInListItem(
62+
name=item["name"],
63+
artifact_id=UUID(item["artifact_id"]),
64+
category=item["category"],
65+
fields=item["fields"],
66+
)
67+
for item in data.get("lists", [])
68+
]
69+
70+
71+
async def use_built_in_list(
72+
artifact_id: UUID,
73+
session: Session,
74+
session_id: UUID | None = None,
75+
) -> UseBuiltInListResult:
76+
"""Copy a built-in list into a session, ready for use in operations.
77+
78+
Args:
79+
artifact_id: The artifact_id from browse results.
80+
session: Session object (provides client and session_id).
81+
session_id: Optional override session_id. Defaults to session.session_id.
82+
83+
Returns:
84+
UseBuiltInListResult with the new artifact_id, session_id, and task_id.
85+
"""
86+
body = {
87+
"artifact_id": str(artifact_id),
88+
"session_id": str(session_id or session.session_id),
89+
}
90+
91+
response = await session.client.get_async_httpx_client().request(
92+
method="POST",
93+
url="/built-in-lists/use",
94+
json=body,
95+
)
96+
if response.status_code != 200:
97+
raise EveryrowError(f"Failed to use built-in list: {response.text}")
98+
99+
data = response.json()
100+
return UseBuiltInListResult(
101+
artifact_id=UUID(data["artifact_id"]),
102+
session_id=UUID(data["session_id"]),
103+
task_id=UUID(data["task_id"]),
104+
)

0 commit comments

Comments
 (0)