Skip to content

Commit 8cf7006

Browse files
jackwildmangithub-actions[bot]
authored andcommitted
feat: allow session_name with session_id to rename sessions (#5056)
## Summary - MCP agents (everyrow-cc) naturally pass both `session_id` and `session_name` when resuming sessions — this previously threw a `ValueError` - Instead of rejecting it, we now use `session_name` as a rename: the session is resumed via `session_id`, and renamed to `session_name` - Adds `PATCH /sessions/{id}` to the public API v0, backed by the existing `update_session_from_spec()` DB function ## Changes - **Engine API**: New `PATCH /sessions/{session_id}` endpoint + `UpdateSession` request model - **Generated client**: New `update_session_endpoint` module + `UpdateSession` attrs model - **SDK `create_session()`**: Fires a PATCH rename when both `session_id` and `name` are provided - **MCP models**: Removed `_check_session_exclusivity` and all 4 call sites; updated 8 field descriptions - **MCP instructions**: Updated "Session and artifact reuse" section to document rename behavior - **Tests**: Converted 5 rejection tests → acceptance/rename verification tests ## Test plan - [x] `pytest tests/test_session.py` — 17 passed - [x] `pytest futuresearch-mcp/tests/test_server.py -k session` — 24 passed - [x] Full SDK suite — 40 passed - [x] Full MCP suite — 356 passed - [x] Pre-commit hooks (format, lint, typecheck) — all passed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Sourced from commit 9f76f1f3886fea1766b58d9e23c32bb4ab936a73
1 parent 1551ad3 commit 8cf7006

File tree

8 files changed

+337
-85
lines changed

8 files changed

+337
-85
lines changed

futuresearch-mcp/src/futuresearch_mcp/app.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,11 @@ async def no_auth_http_lifespan(_server: FastMCP):
116116
117117
## Session and artifact reuse
118118
119-
Every operation creates a session. After your first operation or upload, pass the \
120-
returned `session_id` to subsequent operations to keep tasks grouped. When an \
121-
operation completes, its `artifact_id` can be passed directly to the next operation \
122-
instead of re-uploading data.
119+
Every operation creates a session. After your first operation or upload, **always pass \
120+
the returned `session_id`** to subsequent operations to keep tasks grouped. You may \
121+
pass `session_id` together with `session_name` — the session is resumed and renamed \
122+
to the given name. When an operation completes, its `artifact_id` can be passed \
123+
directly to the next operation instead of re-uploading data.
123124
124125
## Key rules
125126
- Be concise. Keep summaries to 1-2 sentences. Do not output markdown tables, \

futuresearch-mcp/src/futuresearch_mcp/models.py

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -134,18 +134,6 @@ def _validate_session_id(v: str | None) -> str | None:
134134
return v
135135

136136

137-
def _check_session_exclusivity(
138-
session_id: str | None, session_name: str | None
139-
) -> None:
140-
"""Raise if both session_id and session_name are provided."""
141-
if session_id is not None and session_name is not None:
142-
raise ValueError(
143-
"session_id and session_name are mutually exclusive — "
144-
"pass session_id to resume an existing session, "
145-
"or session_name to create a new one."
146-
)
147-
148-
149137
class _SingleSourceInput(BaseModel):
150138
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
151139

@@ -159,11 +147,11 @@ class _SingleSourceInput(BaseModel):
159147
)
160148
session_id: str | None = Field(
161149
default=None,
162-
description="Session ID (UUID) to resume. Mutually exclusive with session_name.",
150+
description="Session ID (UUID) to add to an existing session. If session_name is also provided, the session is renamed.",
163151
)
164152
session_name: str | None = Field(
165153
default=None,
166-
description="Human-readable name for a new session. Mutually exclusive with session_id.",
154+
description="Human-readable name for a new session. If session_id is also provided, renames the existing session.",
167155
)
168156

169157
@field_validator("artifact_id")
@@ -202,7 +190,6 @@ def check_input_source(self):
202190
field_names=("artifact_id", "data"),
203191
label="Input",
204192
)
205-
_check_session_exclusivity(self.session_id, self.session_name)
206193
return self
207194

208195
@property
@@ -377,11 +364,11 @@ class MergeInput(BaseModel):
377364

378365
session_id: str | None = Field(
379366
default=None,
380-
description="Session ID (UUID) to resume. Mutually exclusive with session_name.",
367+
description="Session ID (UUID) to add to an existing session. If session_name is also provided, the session is renamed.",
381368
)
382369
session_name: str | None = Field(
383370
default=None,
384-
description="Human-readable name for a new session. Mutually exclusive with session_id.",
371+
description="Human-readable name for a new session. If session_id is also provided, renames the existing session.",
385372
)
386373

387374
@field_validator("left_artifact_id", "right_artifact_id")
@@ -425,7 +412,6 @@ def check_sources(self) -> "MergeInput":
425412
field_names=("right_artifact_id", "right_data"),
426413
label="Right table",
427414
)
428-
_check_session_exclusivity(self.session_id, self.session_name)
429415
return self
430416

431417
@property
@@ -524,11 +510,11 @@ class UploadDataInput(BaseModel):
524510
)
525511
session_id: str | None = Field(
526512
default=None,
527-
description="Session ID (UUID) to resume. Mutually exclusive with session_name.",
513+
description="Session ID (UUID) to add to an existing session. If session_name is also provided, the session is renamed.",
528514
)
529515
session_name: str | None = Field(
530516
default=None,
531-
description="Human-readable name for a new session. Mutually exclusive with session_id.",
517+
description="Human-readable name for a new session. If session_id is also provided, renames the existing session.",
532518
)
533519

534520
@field_validator("source")
@@ -553,11 +539,6 @@ def validate_source(cls, v: str) -> str:
553539
def validate_session_id(cls, v: str | None) -> str | None:
554540
return _validate_session_id(v)
555541

556-
@model_validator(mode="after")
557-
def check_session_exclusivity(self) -> "UploadDataInput":
558-
_check_session_exclusivity(self.session_id, self.session_name)
559-
return self
560-
561542

562543
class SingleAgentInput(BaseModel):
563544
"""Input for a single agent operation (no CSV)."""
@@ -604,11 +585,11 @@ class SingleAgentInput(BaseModel):
604585
)
605586
session_id: str | None = Field(
606587
default=None,
607-
description="Session ID (UUID) to resume. Mutually exclusive with session_name.",
588+
description="Session ID (UUID) to add to an existing session. If session_name is also provided, the session is renamed.",
608589
)
609590
session_name: str | None = Field(
610591
default=None,
611-
description="Human-readable name for a new session. Mutually exclusive with session_id.",
592+
description="Human-readable name for a new session. If session_id is also provided, renames the existing session.",
612593
)
613594

614595
@field_validator("response_schema")
@@ -623,11 +604,6 @@ def validate_response_schema(
623604
def validate_session_id(cls, v: str | None) -> str | None:
624605
return _validate_session_id(v)
625606

626-
@model_validator(mode="after")
627-
def check_session_exclusivity(self) -> "SingleAgentInput":
628-
_check_session_exclusivity(self.session_id, self.session_name)
629-
return self
630-
631607

632608
def _validate_task_id(v: str) -> str:
633609
"""Validate task_id is a valid UUID."""

futuresearch-mcp/tests/test_server.py

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,14 +1635,16 @@ def test_single_source_accepts_session_name(self):
16351635
)
16361636
assert params.session_name == "My Session"
16371637

1638-
def test_single_source_rejects_both_session_params(self):
1639-
with pytest.raises(ValidationError, match="mutually exclusive"):
1640-
AgentInput(
1641-
task="test",
1642-
artifact_id=str(uuid4()),
1643-
session_id=str(uuid4()),
1644-
session_name="conflict",
1645-
)
1638+
def test_single_source_accepts_both_session_params(self):
1639+
sid = str(uuid4())
1640+
params = AgentInput(
1641+
task="test",
1642+
artifact_id=str(uuid4()),
1643+
session_id=sid,
1644+
session_name="also-provided",
1645+
)
1646+
assert params.session_id == sid
1647+
assert params.session_name == "also-provided"
16461648

16471649
def test_single_source_rejects_invalid_session_id(self):
16481650
with pytest.raises(ValidationError, match="session_id must be a valid UUID"):
@@ -1661,15 +1663,17 @@ def test_merge_accepts_session_id(self):
16611663
)
16621664
assert params.session_id is not None
16631665

1664-
def test_merge_rejects_both_session_params(self):
1665-
with pytest.raises(ValidationError, match="mutually exclusive"):
1666-
MergeInput(
1667-
task="match",
1668-
left_data=[{"a": 1}],
1669-
right_data=[{"b": 2}],
1670-
session_id=str(uuid4()),
1671-
session_name="conflict",
1672-
)
1666+
def test_merge_accepts_both_session_params(self):
1667+
sid = str(uuid4())
1668+
params = MergeInput(
1669+
task="match",
1670+
left_data=[{"a": 1}],
1671+
right_data=[{"b": 2}],
1672+
session_id=sid,
1673+
session_name="also-provided",
1674+
)
1675+
assert params.session_id == sid
1676+
assert params.session_name == "also-provided"
16731677

16741678
def test_single_agent_accepts_session_id(self):
16751679
params = SingleAgentInput(task="test", session_id=str(uuid4()))
@@ -1692,27 +1696,31 @@ def test_single_agent_accepts_custom_params(self):
16921696
assert params.iteration_budget == 5
16931697
assert params.include_reasoning is True
16941698

1695-
def test_single_agent_rejects_both_session_params(self):
1696-
with pytest.raises(ValidationError, match="mutually exclusive"):
1697-
SingleAgentInput(
1698-
task="test",
1699-
session_id=str(uuid4()),
1700-
session_name="conflict",
1701-
)
1699+
def test_single_agent_accepts_both_session_params(self):
1700+
sid = str(uuid4())
1701+
params = SingleAgentInput(
1702+
task="test",
1703+
session_id=sid,
1704+
session_name="also-provided",
1705+
)
1706+
assert params.session_id == sid
1707+
assert params.session_name == "also-provided"
17021708

17031709
def test_upload_data_accepts_session_id(self):
17041710
params = UploadDataInput(
17051711
source="https://example.com/data.csv", session_id=str(uuid4())
17061712
)
17071713
assert params.session_id is not None
17081714

1709-
def test_upload_data_rejects_both_session_params(self):
1710-
with pytest.raises(ValidationError, match="mutually exclusive"):
1711-
UploadDataInput(
1712-
source="https://example.com/data.csv",
1713-
session_id=str(uuid4()),
1714-
session_name="conflict",
1715-
)
1715+
def test_upload_data_accepts_both_session_params(self):
1716+
sid = str(uuid4())
1717+
params = UploadDataInput(
1718+
source="https://example.com/data.csv",
1719+
session_id=sid,
1720+
session_name="also-provided",
1721+
)
1722+
assert params.session_id == sid
1723+
assert params.session_name == "also-provided"
17161724

17171725
# ── Tool invocations ─────────────────────────────────────
17181726

0 commit comments

Comments
 (0)