Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ async with create_session(name="Lead Qualification") as session:

The session URL lets you monitor progress and inspect results in the web UI while your script runs.

### Listing Sessions

Retrieve all your sessions programmatically with `list_sessions`:

```python
from everyrow import list_sessions

sessions = await list_sessions()
for s in sessions:
print(f"{s.name} ({s.session_id}) — created {s.created_at:%Y-%m-%d}")
print(f" View: {s.get_url()}")
```

Each item is a `SessionInfo` with `session_id`, `name`, `created_at`, and `updated_at` fields.

## Async Operations

For long-running jobs, use the `_async` variants to submit work and continue without blocking:
Expand Down
6 changes: 6 additions & 0 deletions docs/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ Cancel a running task. Use when the user wants to stop a task that is currently

Returns a confirmation message. If the task has already finished, returns an error with its current state.

### everyrow_list_sessions

List all sessions owned by the authenticated user. Returns session names, IDs, timestamps, and dashboard URLs. No parameters required.

Returns a formatted list of sessions with links to the web dashboard.

## Workflow

```
Expand Down
4 changes: 4 additions & 0 deletions everyrow-mcp/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@
"name": "everyrow_results",
"description": "Retrieve results from a completed everyrow task and save them to a CSV."
},
{
"name": "everyrow_list_sessions",
"description": "List all everyrow sessions owned by the authenticated user."
},
{
"name": "everyrow_cancel",
"description": "Cancel a running everyrow task. Use when the user wants to stop a task that is currently processing."
Expand Down
41 changes: 40 additions & 1 deletion everyrow-mcp/src/everyrow_mcp/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
screen_async,
single_agent_async,
)
from everyrow.session import create_session, get_session_url
from everyrow.session import create_session, get_session_url, list_sessions
from everyrow.task import cancel_task
from mcp.types import TextContent, ToolAnnotations
from pydantic import BaseModel, create_model
Expand Down Expand Up @@ -730,6 +730,45 @@ async def everyrow_results_http(
]


@mcp.tool(
name="everyrow_list_sessions",
structured_output=False,
annotations=ToolAnnotations(
title="List Sessions",
readOnlyHint=True,
destructiveHint=False,
idempotentHint=True,
openWorldHint=False,
),
)
async def everyrow_list_sessions(ctx: EveryRowContext) -> list[TextContent]:
"""List all everyrow sessions owned by the authenticated user.

Returns session names, IDs, timestamps, and dashboard URLs.
Use this to find past sessions or check what's been run.
"""
client = _get_client(ctx)

try:
sessions = await list_sessions(client=client)
except Exception as e:
return [TextContent(type="text", text=f"Error listing sessions: {e!r}")]

if not sessions:
return [TextContent(type="text", text="No sessions found.")]

lines = [f"Found {len(sessions)} session(s):\n"]
for s in sessions:
lines.append(
f"- **{s.name}** (id: {s.session_id})\n"
f" Created: {s.created_at:%Y-%m-%d %H:%M UTC} | "
f"Updated: {s.updated_at:%Y-%m-%d %H:%M UTC}\n"
f" URL: {s.get_url()}"
)

return [TextContent(type="text", text="\n".join(lines))]


@mcp.tool(
name="everyrow_cancel",
structured_output=False,
Expand Down
1 change: 1 addition & 0 deletions everyrow-mcp/tests/test_mcp_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ async def test_list_tools(self, _http_state):
"everyrow_cancel",
"everyrow_dedupe",
"everyrow_forecast",
"everyrow_list_sessions",
"everyrow_merge",
"everyrow_progress",
"everyrow_rank",
Expand Down
113 changes: 113 additions & 0 deletions everyrow-mcp/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
_RESULTS_META,
everyrow_agent,
everyrow_cancel,
everyrow_list_sessions,
everyrow_progress,
everyrow_results_http,
everyrow_results_stdio,
Expand Down Expand Up @@ -816,6 +817,118 @@ async def test_results_http_store_failure_falls_back_to_inline(self):
assert "1 rows" in result[1].text


class TestListSessions:
"""Tests for everyrow_list_sessions."""

@pytest.mark.asyncio
async def test_list_sessions_returns_sessions(self):
"""Test that list_sessions returns formatted session info."""
mock_client = _make_mock_client()
ctx = make_test_context(mock_client)
mock_sessions = [
MagicMock(
session_id=uuid4(),
name="My Session",
created_at=datetime(2025, 6, 1, 12, 0, tzinfo=UTC),
updated_at=datetime(2025, 6, 1, 13, 0, tzinfo=UTC),
get_url=lambda: "https://everyrow.io/sessions/abc",
),
MagicMock(
session_id=uuid4(),
name="Another Session",
created_at=datetime(2025, 6, 2, 10, 0, tzinfo=UTC),
updated_at=datetime(2025, 6, 2, 11, 0, tzinfo=UTC),
get_url=lambda: "https://everyrow.io/sessions/def",
),
]

with patch(
"everyrow_mcp.tools.list_sessions",
new_callable=AsyncMock,
return_value=mock_sessions,
):
result = await everyrow_list_sessions(ctx)

text = result[0].text
assert "2 session(s)" in text
assert "My Session" in text
assert "Another Session" in text

@pytest.mark.asyncio
async def test_list_sessions_empty(self):
"""Test that list_sessions handles no sessions."""
mock_client = _make_mock_client()
ctx = make_test_context(mock_client)

with patch(
"everyrow_mcp.tools.list_sessions",
new_callable=AsyncMock,
return_value=[],
):
result = await everyrow_list_sessions(ctx)

assert "No sessions found" in result[0].text

@pytest.mark.asyncio
async def test_list_sessions_api_error(self):
"""Test that list_sessions handles API errors gracefully."""
mock_client = _make_mock_client()
ctx = make_test_context(mock_client)

with patch(
"everyrow_mcp.tools.list_sessions",
new_callable=AsyncMock,
side_effect=RuntimeError("API error"),
):
result = await everyrow_list_sessions(ctx)

assert "Error listing sessions" in result[0].text

@pytest.mark.asyncio
async def test_list_sessions_passes_client_from_context(self):
"""Test that the tool passes the context client to list_sessions."""
mock_client = _make_mock_client()
ctx = make_test_context(mock_client)

with patch(
"everyrow_mcp.tools.list_sessions",
new_callable=AsyncMock,
return_value=[],
) as mock_ls:
await everyrow_list_sessions(ctx)

mock_ls.assert_called_once_with(client=mock_client)

@pytest.mark.asyncio
async def test_list_sessions_output_contains_urls_and_dates(self):
"""Test that the formatted output includes URLs and timestamps."""
mock_client = _make_mock_client()
ctx = make_test_context(mock_client)
session_id = uuid4()
mock_sessions = [
MagicMock(
session_id=session_id,
name="Pipeline Run",
created_at=datetime(2025, 8, 15, 9, 30, tzinfo=UTC),
updated_at=datetime(2025, 8, 15, 10, 45, tzinfo=UTC),
get_url=lambda: f"https://everyrow.io/sessions/{session_id}",
),
]

with patch(
"everyrow_mcp.tools.list_sessions",
new_callable=AsyncMock,
return_value=mock_sessions,
):
result = await everyrow_list_sessions(ctx)

text = result[0].text
assert "Pipeline Run" in text
assert "2025-08-15 09:30 UTC" in text
assert "2025-08-15 10:45 UTC" in text
assert f"https://everyrow.io/sessions/{session_id}" in text


class TestCancel:
"""Tests for everyrow_cancel."""

Expand Down
4 changes: 3 additions & 1 deletion src/everyrow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

from everyrow.api_utils import create_client
from everyrow.billing import BillingResponse, get_billing_balance
from everyrow.session import create_session
from everyrow.session import SessionInfo, create_session, list_sessions
from everyrow.task import fetch_task_data, print_progress

__version__ = version("everyrow")

__all__ = [
"BillingResponse",
"SessionInfo",
"__version__",
"create_client",
"create_session",
"fetch_task_data",
"get_billing_balance",
"list_sessions",
"print_progress",
]
2 changes: 2 additions & 0 deletions src/everyrow/generated/api/sessions/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
"""Contains endpoint functions for accessing the API"""

from . import list_sessions_endpoint_sessions_get
Loading