openhands-agent-server 1.31.0__tar.gz → 1.31.2__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.
Files changed (71) hide show
  1. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/PKG-INFO +1 -1
  2. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/agent_profiles_router.py +2 -69
  3. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/conversation_service.py +22 -2
  4. openhands_agent_server-1.31.2/openhands/agent_server/event_compat.py +50 -0
  5. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/event_router.py +17 -4
  6. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/event_service.py +22 -14
  7. openhands_agent_server-1.31.2/openhands/agent_server/mcp_oauth_store.py +352 -0
  8. openhands_agent_server-1.31.2/openhands/agent_server/mcp_router.py +824 -0
  9. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/persistence/models.py +9 -3
  10. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/profiles_router.py +3 -7
  11. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/settings_router.py +3 -2
  12. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/sockets.py +3 -2
  13. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/sub_agents_router.py +11 -2
  14. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands_agent_server.egg-info/PKG-INFO +1 -1
  15. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands_agent_server.egg-info/SOURCES.txt +4 -0
  16. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/pyproject.toml +1 -1
  17. openhands_agent_server-1.31.0/openhands/agent_server/mcp_router.py +0 -355
  18. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/__init__.py +0 -0
  19. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/__main__.py +0 -0
  20. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/_secrets_exposure.py +0 -0
  21. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/api.py +0 -0
  22. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/auth_router.py +0 -0
  23. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/bash_router.py +0 -0
  24. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/bash_service.py +0 -0
  25. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/config.py +0 -0
  26. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/conversation_lease.py +0 -0
  27. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/conversation_router.py +0 -0
  28. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/dependencies.py +0 -0
  29. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/desktop_router.py +0 -0
  30. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/desktop_service.py +0 -0
  31. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/docker/Dockerfile +0 -0
  32. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/docker/build.py +0 -0
  33. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/docker/wallpaper.svg +0 -0
  34. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/env_parser.py +0 -0
  35. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/file_router.py +0 -0
  36. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/git_router.py +0 -0
  37. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/hooks_router.py +0 -0
  38. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/hooks_service.py +0 -0
  39. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/init_router.py +0 -0
  40. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/llm_router.py +0 -0
  41. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/logging_config.py +0 -0
  42. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/middleware.py +0 -0
  43. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/models.py +0 -0
  44. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/openai/__init__.py +0 -0
  45. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/openai/models.py +0 -0
  46. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/openai/router.py +0 -0
  47. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/openai/service.py +0 -0
  48. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/openapi.py +0 -0
  49. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/persistence/__init__.py +0 -0
  50. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/persistence/store.py +0 -0
  51. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/plugins_router.py +0 -0
  52. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/plugins_service.py +0 -0
  53. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/pub_sub.py +0 -0
  54. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/py.typed +0 -0
  55. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/server_details_router.py +0 -0
  56. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/skills_router.py +0 -0
  57. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/skills_service.py +0 -0
  58. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/tool_preload_service.py +0 -0
  59. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/tool_router.py +0 -0
  60. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/utils.py +0 -0
  61. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
  62. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
  63. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/vscode_router.py +0 -0
  64. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/vscode_service.py +0 -0
  65. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/workspace_router.py +0 -0
  66. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/workspaces_router.py +0 -0
  67. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
  68. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands_agent_server.egg-info/entry_points.txt +0 -0
  69. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands_agent_server.egg-info/requires.txt +0 -0
  70. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands_agent_server.egg-info/top_level.txt +0 -0
  71. {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.31.0
3
+ Version: 1.31.2
4
4
  Summary: OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -12,7 +12,6 @@ MCP references and returns :class:`~openhands.sdk.profiles.AgentProfileDiagnosti
12
12
  """
13
13
 
14
14
  import asyncio
15
- import copy
16
15
  from collections.abc import Iterator
17
16
  from contextlib import contextmanager
18
17
  from typing import Annotated, Any
@@ -42,10 +41,8 @@ from openhands.sdk.llm.llm_profile_store import (
42
41
  from openhands.sdk.logger import get_logger
43
42
  from openhands.sdk.profiles import (
44
43
  SEED_PROFILE_NAME,
45
- ACPAgentProfile,
46
44
  AgentProfileDiagnostics,
47
45
  AgentProfileStore,
48
- OpenHandsAgentProfile,
49
46
  ProfileLimitExceeded,
50
47
  build_seed_profile,
51
48
  resolve_agent_profile_dry_run,
@@ -55,7 +52,6 @@ from openhands.sdk.profiles import (
55
52
  )
56
53
  from openhands.sdk.profiles.agent_profile_store import PROFILE_NAME_PATTERN
57
54
  from openhands.sdk.utils.cipher import Cipher
58
- from openhands.sdk.utils.pydantic_secrets import decrypt_str_with_cipher_or_keep
59
55
 
60
56
 
61
57
  logger = get_logger(__name__)
@@ -134,54 +130,6 @@ def _store_errors() -> Iterator[None]:
134
130
  raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
135
131
 
136
132
 
137
- def _decrypt_mcp_tools(tools: dict[str, Any], cipher: Cipher) -> dict[str, Any]:
138
- """Return a copy of an ``mcp_tools`` dict with env/headers Fernet tokens
139
- decrypted. Non-Fernet (plaintext) values pass through unchanged."""
140
- servers = tools.get("mcpServers")
141
- if not isinstance(servers, dict):
142
- return tools
143
- out = copy.deepcopy(tools)
144
- for server in out["mcpServers"].values():
145
- if not isinstance(server, dict):
146
- continue
147
- for key in ("env", "headers"):
148
- mapping = server.get(key)
149
- if isinstance(mapping, dict):
150
- server[key] = {
151
- k: decrypt_str_with_cipher_or_keep(
152
- cipher, v, description="MCP env/headers"
153
- )
154
- for k, v in mapping.items()
155
- }
156
- return out
157
-
158
-
159
- def _decrypt_profile_mcp_tools(
160
- profile: OpenHandsAgentProfile | ACPAgentProfile, cipher: Cipher | None
161
- ) -> OpenHandsAgentProfile | ACPAgentProfile:
162
- """Decrypt Fernet ``skills[].mcp_tools`` env/headers (no-op without a cipher).
163
-
164
- The store masks/encrypts these on save but has no symmetric load-time
165
- validator, so values arrive as ciphertext on both GET (at-rest) and save
166
- (client round-trip). Decrypting here lets GET re-mask from plaintext and
167
- stops save from double-encrypting an already-encrypted value.
168
- """
169
- if cipher is None:
170
- return profile
171
- skills = getattr(profile, "skills", None)
172
- if not skills:
173
- return profile
174
- new_skills = [
175
- skill.model_copy(
176
- update={"mcp_tools": _decrypt_mcp_tools(skill.mcp_tools, cipher)}
177
- )
178
- if skill.mcp_tools
179
- else skill
180
- for skill in skills
181
- ]
182
- return profile.model_copy(update={"skills": new_skills})
183
-
184
-
185
133
  def _seed_default_llm_profile(llm: LLM, cipher: Cipher | None) -> str:
186
134
  """Mirror the live LLM config into a ``SEED_PROFILE_NAME`` LLM profile.
187
135
 
@@ -265,9 +213,6 @@ def _seed_default_profile(
265
213
  settings.agent_settings.llm, cipher
266
214
  )
267
215
  profile = build_seed_profile(settings.agent_settings, active_llm_profile)
268
- # Settings persist skills[].mcp_tools encrypted (and never decrypt on
269
- # load), so decrypt before re-encrypting at save to avoid double-encrypt.
270
- profile = _decrypt_profile_mcp_tools(profile, cipher)
271
216
  store.save(profile, cipher=cipher, max_profiles=MAX_AGENT_PROFILES)
272
217
 
273
218
  profile_id = str(profile.id)
@@ -342,10 +287,6 @@ async def get_agent_profile(
342
287
  detail=f"Agent profile '{name}' not found",
343
288
  )
344
289
 
345
- # The store leaves skills[].mcp_tools encrypted on load; decrypt to plaintext
346
- # so the expose serializer can correctly redact / re-encrypt / reveal them.
347
- profile = _decrypt_profile_mcp_tools(profile, cipher)
348
-
349
290
  context = build_expose_context(expose_mode, cipher)
350
291
  with translate_missing_cipher():
351
292
  payload = profile.model_dump(mode="json", context=context)
@@ -374,7 +315,8 @@ async def save_agent_profile(
374
315
  except ValidationError as e:
375
316
  # Match FastAPI's request-validation shape (``detail`` is a list of error
376
317
  # objects), but surface only ``loc``/``type`` — a nested mcp_tools
377
- # MCPConfig error embeds the input (which may carry secrets) in ``msg``.
318
+ # FastMCP config error embeds the input (which may carry secrets) in
319
+ # ``msg``.
378
320
  raise HTTPException(
379
321
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
380
322
  detail=safe_validation_error_detail(e),
@@ -388,11 +330,6 @@ async def save_agent_profile(
388
330
  detail="Invalid agent profile",
389
331
  )
390
332
 
391
- # A client editing a profile fetched with X-Expose-Secrets: encrypted posts
392
- # back Fernet tokens; decrypt them so the save re-encrypts the original
393
- # secret once rather than double-encrypting the token.
394
- profile = _decrypt_profile_mcp_tools(profile, cipher)
395
-
396
333
  store = get_agent_profile_store()
397
334
  # The id is server-managed (the active pointer is keyed on it): overwrite
398
335
  # keeps the namesake's id and bumps revision; create mints a fresh id,
@@ -564,10 +501,6 @@ async def materialize_agent_profile(
564
501
  detail=f"Agent profile '{name}' not found",
565
502
  )
566
503
 
567
- # The store leaves skills[].mcp_tools encrypted on load; decrypt so the
568
- # resolver builds settings from plaintext (not ciphertext) values.
569
- profile = _decrypt_profile_mcp_tools(profile, cipher)
570
-
571
504
  config = get_config(request)
572
505
  settings = get_settings_store(config).load() or PersistedSettings()
573
506
  mcp_config = settings.agent_settings.mcp_config
@@ -48,12 +48,15 @@ from openhands.sdk.event import MessageEvent
48
48
  from openhands.sdk.event.conversation_state import ConversationStateUpdateEvent
49
49
  from openhands.sdk.git.exceptions import GitCommandError, GitRepositoryError
50
50
  from openhands.sdk.git.utils import run_git_command, validate_git_repository
51
+ from openhands.sdk.mcp.utils import MCPToolProvider
52
+ from openhands.sdk.tool import BROWSER_TOOL_NAME, Tool, is_tool_usable
51
53
  from openhands.sdk.tool.client_tool import register_client_tools
52
54
  from openhands.sdk.utils.cipher import Cipher
53
55
  from openhands.sdk.workspace import LocalWorkspace
54
56
 
55
57
 
56
58
  if TYPE_CHECKING:
59
+ from openhands.sdk.mcp.config import MCPServer
57
60
  from openhands.sdk.subagent.schema import AgentDefinition
58
61
 
59
62
  CONVERSATION_WORKTREE_ROOT = Path("/tmp/conversation-worktrees")
@@ -249,14 +252,14 @@ logger = logging.getLogger(__name__)
249
252
  def _resolve_agent_from_profile(
250
253
  profile_id: "UUID",
251
254
  cipher: "Cipher | None",
252
- mcp_config: "Any",
255
+ mcp_config: "dict[str, MCPServer]",
253
256
  ) -> "tuple[AgentBase, LaunchedAgentProfile]":
254
257
  """Load and resolve an agent profile by id, returning the built agent + provenance.
255
258
 
256
259
  Runs synchronously (call via ``asyncio.to_thread`` from async context).
257
260
 
258
261
  Args:
259
- mcp_config: Global MCP config already loaded by the caller using the
262
+ mcp_config: Global MCP servers already loaded by the caller using the
260
263
  server's cipher. Passed explicitly so this free function never
261
264
  touches the settings-store singleton (which may not have been
262
265
  initialised with the correct cipher yet).
@@ -312,6 +315,17 @@ def _resolve_agent_from_profile(
312
315
  raise ValueError(f"Profile '{profile_name}' failed to resolve: {exc}") from exc
313
316
 
314
317
  agent = settings_config.create_agent()
318
+ # Browser is deliberately absent from the deterministic SDK default
319
+ # (environment-dependent); this server knows its runtime, so it injects
320
+ # browser when usable. An explicit profile.tools list is authoritative.
321
+ if (
322
+ profile.agent_kind == "openhands"
323
+ and profile.tools is None
324
+ and is_tool_usable(BROWSER_TOOL_NAME)
325
+ ):
326
+ agent = agent.model_copy(
327
+ update={"tools": [*agent.tools, Tool(name=BROWSER_TOOL_NAME)]}
328
+ )
315
329
  launched = LaunchedAgentProfile(
316
330
  agent_profile_id=profile.id,
317
331
  revision=profile.revision,
@@ -461,6 +475,7 @@ class ConversationService:
461
475
  webhook_specs: list[WebhookSpec] = field(default_factory=list)
462
476
  session_api_key: str | None = field(default=None)
463
477
  cipher: Cipher | None = None
478
+ mcp_tool_provider: MCPToolProvider | None = None
464
479
  owner_instance_id: str = field(default_factory=lambda: uuid4().hex)
465
480
  max_concurrent_runs: int = 10
466
481
  lease_ttl_seconds: float = DEFAULT_LEASE_TTL_SECONDS
@@ -1256,6 +1271,9 @@ class ConversationService:
1256
1271
  def get_instance(cls, config: Config) -> "ConversationService":
1257
1272
  # Initialise the settings-store singleton with the server cipher before
1258
1273
  # any conversation handler can call get_settings_store() without config.
1274
+ from openhands.agent_server.mcp_oauth_store import (
1275
+ create_settings_backed_mcp_tool_provider,
1276
+ )
1259
1277
  from openhands.agent_server.persistence import get_settings_store
1260
1278
 
1261
1279
  get_settings_store(config)
@@ -1266,6 +1284,7 @@ class ConversationService:
1266
1284
  config.session_api_keys[0] if config.session_api_keys else None
1267
1285
  ),
1268
1286
  cipher=config.cipher,
1287
+ mcp_tool_provider=create_settings_backed_mcp_tool_provider(config),
1269
1288
  max_concurrent_runs=config.max_concurrent_runs,
1270
1289
  lease_ttl_seconds=config.lease_ttl_seconds,
1271
1290
  )
@@ -1279,6 +1298,7 @@ class ConversationService:
1279
1298
  stored=stored,
1280
1299
  conversations_dir=self.conversations_dir,
1281
1300
  cipher=self.cipher,
1301
+ mcp_tool_provider=self.mcp_tool_provider,
1282
1302
  owner_instance_id=self.owner_instance_id,
1283
1303
  lease_ttl_seconds=self.lease_ttl_seconds,
1284
1304
  )
@@ -0,0 +1,50 @@
1
+ """Compatibility helpers for event transport payloads."""
2
+
3
+ from typing import Any, Final
4
+
5
+ from openhands.sdk.event import Event
6
+
7
+
8
+ _LEGACY_TOOL_KINDS: Final[set[str]] = {
9
+ "ClientTool",
10
+ "FileEditorTool",
11
+ "FinishTool",
12
+ "InvokeSkillTool",
13
+ "MCPToolDefinition",
14
+ "SwitchLLMTool",
15
+ "TaskTool",
16
+ "TaskToolSet",
17
+ "TaskTrackerTool",
18
+ "TerminalTool",
19
+ "ThinkTool",
20
+ "WorkflowTool",
21
+ "WorkflowToolSet",
22
+ }
23
+
24
+
25
+ def event_transport_dump(event: Event | dict[str, Any]) -> dict[str, Any]:
26
+ """Serialize an event for HTTP/WebSocket clients across SDK versions.
27
+
28
+ Some deployed clients run older SDKs than the agent-server. They parse event
29
+ payloads with strict pydantic unions, so additive fields and new tool kinds
30
+ can make otherwise completed conversations look failed client-side. Keep
31
+ persistence untouched and apply this compatibility envelope only at the API
32
+ transport boundary.
33
+ """
34
+ if isinstance(event, dict):
35
+ dumped = dict(event)
36
+ else:
37
+ dumped = event.model_dump(mode="json", exclude_none=True)
38
+ dumped.pop("parent_id", None)
39
+
40
+ tools = dumped.get("tools")
41
+ if isinstance(tools, list):
42
+ dumped["tools"] = [
43
+ tool
44
+ for tool in tools
45
+ if not isinstance(tool, dict)
46
+ or tool.get("kind") is None
47
+ or tool.get("kind") in _LEGACY_TOOL_KINDS
48
+ ]
49
+
50
+ return dumped
@@ -4,7 +4,7 @@ Local Event router for OpenHands SDK.
4
4
 
5
5
  import logging
6
6
  from datetime import datetime
7
- from typing import Annotated
7
+ from typing import Annotated, Any, cast
8
8
 
9
9
  from fastapi import (
10
10
  APIRouter,
@@ -13,12 +13,13 @@ from fastapi import (
13
13
  Query,
14
14
  status,
15
15
  )
16
+ from starlette.responses import JSONResponse
16
17
 
17
18
  from openhands.agent_server.dependencies import get_event_service
19
+ from openhands.agent_server.event_compat import event_transport_dump
18
20
  from openhands.agent_server.event_service import EventService
19
21
  from openhands.agent_server.models import (
20
22
  ConfirmationResponseRequest,
21
- EventPage,
22
23
  EventSortOrder,
23
24
  SendMessageRequest,
24
25
  Success,
@@ -99,7 +100,7 @@ async def search_conversation_events(
99
100
  Query(title="Filter: event timestamp < this datetime"),
100
101
  ] = None,
101
102
  event_service: EventService = Depends(get_event_service),
102
- ) -> EventPage:
103
+ ) -> JSONResponse:
103
104
  """Search / List local events"""
104
105
  assert limit > 0
105
106
  assert limit <= 100
@@ -114,9 +115,21 @@ async def search_conversation_events(
114
115
  normalize_datetime_to_server_timezone(timestamp__lt) if timestamp__lt else None
115
116
  )
116
117
 
117
- return await event_service.search_events(
118
+ page = await event_service.search_events(
118
119
  page_id, limit, kind, source, body, sort_order, normalized_gte, normalized_lt
119
120
  )
121
+ if isinstance(page, dict):
122
+ items = cast(list[Any], page.get("items", []))
123
+ next_page_id = cast(str | None, page.get("next_page_id"))
124
+ else:
125
+ items = page.items
126
+ next_page_id = page.next_page_id
127
+ return JSONResponse(
128
+ {
129
+ "items": [event_transport_dump(event) for event in items],
130
+ "next_page_id": next_page_id,
131
+ }
132
+ )
120
133
 
121
134
 
122
135
  @event_router.get("/count", responses={404: {"description": "Conversation not found"}})
@@ -55,6 +55,7 @@ from openhands.sdk.event.llm_completion_log import LLMCompletionLogEvent
55
55
  from openhands.sdk.git.exceptions import GitCommandError, GitRepositoryError
56
56
  from openhands.sdk.git.utils import run_git_command, validate_git_repository
57
57
  from openhands.sdk.llm.streaming import LLMStreamChunk
58
+ from openhands.sdk.mcp.utils import MCPToolProvider
58
59
  from openhands.sdk.security.analyzer import SecurityAnalyzerBase
59
60
  from openhands.sdk.security.confirmation_policy import ConfirmationPolicyBase
60
61
  from openhands.sdk.utils.async_utils import AsyncCallbackWrapper
@@ -81,6 +82,7 @@ class EventService:
81
82
  stored: StoredConversation
82
83
  conversations_dir: Path
83
84
  cipher: Cipher | None = None
85
+ mcp_tool_provider: MCPToolProvider | None = None
84
86
  owner_instance_id: str = field(default_factory=lambda: uuid4().hex)
85
87
  lease_ttl_seconds: float = DEFAULT_LEASE_TTL_SECONDS
86
88
  _conversation: LocalConversation | None = field(default=None, init=False)
@@ -579,21 +581,26 @@ class EventService:
579
581
  # conversation's synchronous FIFOLock cannot block the server event loop.
580
582
  if self._conversation:
581
583
  state_update_event = await self._create_state_update_event()
584
+ else:
585
+ state_update_event = ConversationStateUpdateEvent(
586
+ key="execution_status",
587
+ value=ConversationExecutionStatus.IDLE,
588
+ )
582
589
 
583
- try:
584
- await asyncio.wait_for(
585
- subscriber(state_update_event),
586
- timeout=INITIAL_STATE_PUSH_TIMEOUT_SECONDS,
587
- )
588
- except TimeoutError:
589
- # Subscriber stays registered; only the initial-state push is
590
- # dropped. Subsequent publishes go through pub_sub and may
591
- # still block there if the subscriber remains wedged.
592
- logger.warning(
593
- f"Initial state push to subscriber {subscriber_id} timed "
594
- f"out after {INITIAL_STATE_PUSH_TIMEOUT_SECONDS}s."
595
- )
596
- # Non-timeout errors propagate to caller (e.g. webhook failures).
590
+ try:
591
+ await asyncio.wait_for(
592
+ subscriber(state_update_event),
593
+ timeout=INITIAL_STATE_PUSH_TIMEOUT_SECONDS,
594
+ )
595
+ except TimeoutError:
596
+ # Subscriber stays registered; only the initial-state push is
597
+ # dropped. Subsequent publishes go through pub_sub and may
598
+ # still block there if the subscriber remains wedged.
599
+ logger.warning(
600
+ f"Initial state push to subscriber {subscriber_id} timed "
601
+ f"out after {INITIAL_STATE_PUSH_TIMEOUT_SECONDS}s."
602
+ )
603
+ # Non-timeout errors propagate to caller (e.g. webhook failures).
597
604
 
598
605
  return subscriber_id
599
606
 
@@ -832,6 +839,7 @@ class EventService:
832
839
  observability_metadata=self.stored.observability_metadata,
833
840
  observability_tags=self.stored.observability_tags,
834
841
  observability_span_name=self.stored.observability_span_name,
842
+ mcp_tool_provider=self.mcp_tool_provider,
835
843
  )
836
844
 
837
845
  conversation.set_confirmation_policy(self.stored.confirmation_policy)