Skip to content
Closed
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
6 changes: 6 additions & 0 deletions everyrow-mcp/src/everyrow_mcp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ class Settings(BaseSettings):

everyrow_api_key: str | None = Field(default=None, repr=False)

require_auth: bool = Field(
default=True,
description="Require OAuth authentication in HTTP mode. "
"Set to False via --no-auth for local development.",
)

@property
def is_http(self) -> bool:
return self.transport == "streamable-http"
Expand Down
2 changes: 2 additions & 0 deletions everyrow-mcp/src/everyrow_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ def main():
os.environ["EVERYROW_MCP_SERVER"] = "1"
transport = Transport.HTTP if input_args.http else Transport.STDIO
settings.transport = transport.value
if input_args.no_auth:
settings.require_auth = False
mcp._mcp_server.instructions = get_instructions(is_http=input_args.http)

# tools.py registers everyrow_results_stdio by default.
Expand Down
9 changes: 7 additions & 2 deletions everyrow-mcp/src/everyrow_mcp/tool_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,13 @@ async def create_tool_response(
"""Build the standard submission response for a tool.

Returns human-readable text in all modes, plus a widget JSON
prepended in HTTP mode.
prepended in authenticated HTTP mode. In no-auth HTTP mode the
widget is skipped (no Claude Desktop to render it) but the task
token is still stored so the progress tool can poll the API.
"""
text = _submission_text(label, session_url, task_id)
main_content = TextContent(type="text", text=text)
if settings.is_http:
if settings.is_http and settings.require_auth:
ui_json = await _submission_ui_json(
session_url=session_url,
task_id=task_id,
Comment on lines +142 to 145

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: In no-auth HTTP mode, task submission correctly skips storing an owner, but follow-up calls like everyrow_progress incorrectly still perform an ownership check, causing them to fail.
Severity: MEDIUM

Suggested Fix

In tools.py, update the condition in _check_task_ownership() to also check for settings.require_auth. Change if not settings.is_http: to if not settings.is_http or not settings.require_auth:. This will bypass the ownership check when authentication is disabled, aligning it with the task submission logic.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: everyrow-mcp/src/everyrow_mcp/tool_helpers.py#L142-L145

Potential issue: The new code correctly skips recording task ownership when running in
no-auth HTTP mode (`require_auth=False`). However, the corresponding ownership check in
`_check_task_ownership()` was not updated. This function is called by tools like
`everyrow_progress()` and `everyrow_results_http()`. It still attempts to verify
ownership if `settings.is_http` is true, without considering `settings.require_auth`. As
a result, after submitting a task in no-auth mode, any attempt to check its progress or
retrieve results will fail with an "Access denied" error because no owner was ever
stored.

Did we get this right? 👍 / 👎 to inform future reviews.

Expand All @@ -146,6 +148,9 @@ async def create_tool_response(
mcp_server_url=mcp_server_url,
)
return [TextContent(type="text", text=ui_json), main_content]
if settings.is_http:
# No-auth HTTP: store the API token for progress polling, skip widget.
await redis_store.store_task_token(task_id, token)
return [main_content]


Expand Down
80 changes: 80 additions & 0 deletions everyrow-mcp/tests/test_mcp_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,41 @@ def _http_state(fake_redis):
)(everyrow_results_stdio)


@pytest.fixture
def _noauth_http_state(fake_redis):
"""Configure settings for HTTP no-auth mode and patch Redis.

Like _http_state but with require_auth=False and no access-token patching,
matching the --no-auth server mode.
"""
mcp_app._tool_manager.remove_tool("everyrow_results")
mcp_app.tool(
name="everyrow_results",
structured_output=False,
annotations=_RESULTS_ANNOTATIONS,
meta=_RESULTS_META,
)(everyrow_results_http)

with (
override_settings(
transport="streamable-http",
upload_secret="test-secret",
require_auth=False,
),
patch.object(redis_store, "get_redis_client", return_value=fake_redis),
):
yield

# Restore stdio variant
mcp_app._tool_manager.remove_tool("everyrow_results")
mcp_app.tool(
name="everyrow_results",
structured_output=False,
annotations=_RESULTS_ANNOTATIONS,
meta=_RESULTS_META,
)(everyrow_results_stdio)


@asynccontextmanager
async def mcp_client():
"""MCP ClientSession connected to the server via in-memory transport.
Expand Down Expand Up @@ -370,6 +405,51 @@ async def test_completed_progress_suggests_results(self, _http_state):
human_text = result.content[-1].text
assert "everyrow_results" in human_text

@pytest.mark.asyncio
async def test_submit_task_noauth_http(self, _noauth_http_state):
"""Task submission works in no-auth HTTP mode (no access token).

No widget JSON is returned (no Claude Desktop to render it),
only the human-readable text.
"""
task_id = str(uuid4())
mock_task = _mock_task(task_id)
_, fake_create_session = _mock_session()

async with mcp_client() as session:
with (
patch(
"everyrow_mcp.tools._get_client",
return_value=MagicMock(token="fake-token"),
),
patch(
"everyrow_mcp.tools.screen_async",
new_callable=AsyncMock,
return_value=mock_task,
),
patch(
"everyrow_mcp.tools.create_session",
side_effect=fake_create_session,
),
):
result = await session.call_tool(
"everyrow_screen",
{
"params": {
"task": "Filter for engineering roles",
"data": [
{"company": "Acme", "role": "Engineer"},
],
}
},
)

assert not result.isError
# No-auth mode: text only, no widget JSON
assert len(result.content) == 1
assert isinstance(result.content[0], TextContent)
assert task_id in result.content[0].text


# ── TestMcpE2ERealApi — real API tests ────────────────────────
_skip_unless_integration = pytest.mark.skipif(
Expand Down