agentpool 2.1.9__py3-none-any.whl → 2.5.0__py3-none-any.whl

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 (311) hide show
  1. acp/__init__.py +13 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/bridge/README.md +15 -2
  7. acp/bridge/__init__.py +3 -2
  8. acp/bridge/__main__.py +60 -19
  9. acp/bridge/ws_server.py +173 -0
  10. acp/bridge/ws_server_cli.py +89 -0
  11. acp/client/connection.py +38 -29
  12. acp/client/implementations/default_client.py +3 -2
  13. acp/client/implementations/headless_client.py +2 -2
  14. acp/connection.py +2 -2
  15. acp/notifications.py +20 -50
  16. acp/schema/__init__.py +2 -0
  17. acp/schema/agent_responses.py +21 -0
  18. acp/schema/client_requests.py +3 -3
  19. acp/schema/session_state.py +63 -29
  20. acp/stdio.py +39 -9
  21. acp/task/supervisor.py +2 -2
  22. acp/transports.py +362 -2
  23. acp/utils.py +17 -4
  24. agentpool/__init__.py +6 -1
  25. agentpool/agents/__init__.py +2 -0
  26. agentpool/agents/acp_agent/acp_agent.py +407 -277
  27. agentpool/agents/acp_agent/acp_converters.py +196 -38
  28. agentpool/agents/acp_agent/client_handler.py +191 -26
  29. agentpool/agents/acp_agent/session_state.py +17 -6
  30. agentpool/agents/agent.py +607 -572
  31. agentpool/agents/agui_agent/__init__.py +0 -2
  32. agentpool/agents/agui_agent/agui_agent.py +176 -110
  33. agentpool/agents/agui_agent/agui_converters.py +0 -131
  34. agentpool/agents/agui_agent/helpers.py +3 -4
  35. agentpool/agents/base_agent.py +632 -17
  36. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  37. agentpool/agents/claude_code_agent/__init__.py +13 -1
  38. agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
  39. agentpool/agents/claude_code_agent/converters.py +74 -143
  40. agentpool/agents/claude_code_agent/history.py +474 -0
  41. agentpool/agents/claude_code_agent/models.py +77 -0
  42. agentpool/agents/claude_code_agent/static_info.py +100 -0
  43. agentpool/agents/claude_code_agent/usage.py +242 -0
  44. agentpool/agents/context.py +40 -0
  45. agentpool/agents/events/__init__.py +24 -0
  46. agentpool/agents/events/builtin_handlers.py +67 -1
  47. agentpool/agents/events/event_emitter.py +32 -2
  48. agentpool/agents/events/events.py +104 -3
  49. agentpool/agents/events/infer_info.py +145 -0
  50. agentpool/agents/events/processors.py +254 -0
  51. agentpool/agents/interactions.py +41 -6
  52. agentpool/agents/modes.py +67 -0
  53. agentpool/agents/slashed_agent.py +5 -4
  54. agentpool/agents/tool_call_accumulator.py +213 -0
  55. agentpool/agents/tool_wrapping.py +18 -6
  56. agentpool/common_types.py +56 -21
  57. agentpool/config_resources/__init__.py +38 -1
  58. agentpool/config_resources/acp_assistant.yml +2 -2
  59. agentpool/config_resources/agents.yml +3 -0
  60. agentpool/config_resources/agents_template.yml +1 -0
  61. agentpool/config_resources/claude_code_agent.yml +10 -6
  62. agentpool/config_resources/external_acp_agents.yml +2 -1
  63. agentpool/delegation/base_team.py +4 -30
  64. agentpool/delegation/pool.py +136 -289
  65. agentpool/delegation/team.py +58 -57
  66. agentpool/delegation/teamrun.py +51 -55
  67. agentpool/diagnostics/__init__.py +53 -0
  68. agentpool/diagnostics/lsp_manager.py +1593 -0
  69. agentpool/diagnostics/lsp_proxy.py +41 -0
  70. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  71. agentpool/diagnostics/models.py +398 -0
  72. agentpool/functional/run.py +10 -4
  73. agentpool/mcp_server/__init__.py +0 -2
  74. agentpool/mcp_server/client.py +76 -32
  75. agentpool/mcp_server/conversions.py +54 -13
  76. agentpool/mcp_server/manager.py +34 -54
  77. agentpool/mcp_server/registries/official_registry_client.py +35 -1
  78. agentpool/mcp_server/tool_bridge.py +186 -139
  79. agentpool/messaging/__init__.py +0 -2
  80. agentpool/messaging/compaction.py +72 -197
  81. agentpool/messaging/connection_manager.py +11 -10
  82. agentpool/messaging/event_manager.py +5 -5
  83. agentpool/messaging/message_container.py +6 -30
  84. agentpool/messaging/message_history.py +99 -8
  85. agentpool/messaging/messagenode.py +52 -14
  86. agentpool/messaging/messages.py +54 -35
  87. agentpool/messaging/processing.py +12 -22
  88. agentpool/models/__init__.py +1 -1
  89. agentpool/models/acp_agents/base.py +6 -24
  90. agentpool/models/acp_agents/mcp_capable.py +126 -157
  91. agentpool/models/acp_agents/non_mcp.py +129 -95
  92. agentpool/models/agents.py +98 -76
  93. agentpool/models/agui_agents.py +1 -1
  94. agentpool/models/claude_code_agents.py +144 -19
  95. agentpool/models/file_parsing.py +0 -1
  96. agentpool/models/manifest.py +113 -50
  97. agentpool/prompts/conversion_manager.py +1 -1
  98. agentpool/prompts/prompts.py +5 -2
  99. agentpool/repomap.py +1 -1
  100. agentpool/resource_providers/__init__.py +11 -1
  101. agentpool/resource_providers/aggregating.py +56 -5
  102. agentpool/resource_providers/base.py +70 -4
  103. agentpool/resource_providers/codemode/code_executor.py +72 -5
  104. agentpool/resource_providers/codemode/helpers.py +2 -2
  105. agentpool/resource_providers/codemode/provider.py +64 -12
  106. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  107. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  108. agentpool/resource_providers/filtering.py +3 -1
  109. agentpool/resource_providers/mcp_provider.py +89 -12
  110. agentpool/resource_providers/plan_provider.py +228 -46
  111. agentpool/resource_providers/pool.py +7 -3
  112. agentpool/resource_providers/resource_info.py +111 -0
  113. agentpool/resource_providers/static.py +4 -2
  114. agentpool/sessions/__init__.py +4 -1
  115. agentpool/sessions/manager.py +33 -5
  116. agentpool/sessions/models.py +59 -6
  117. agentpool/sessions/protocol.py +28 -0
  118. agentpool/sessions/session.py +11 -55
  119. agentpool/skills/registry.py +13 -8
  120. agentpool/storage/manager.py +572 -49
  121. agentpool/talk/registry.py +4 -4
  122. agentpool/talk/talk.py +9 -10
  123. agentpool/testing.py +538 -20
  124. agentpool/tool_impls/__init__.py +6 -0
  125. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  126. agentpool/tool_impls/agent_cli/tool.py +95 -0
  127. agentpool/tool_impls/bash/__init__.py +64 -0
  128. agentpool/tool_impls/bash/helpers.py +35 -0
  129. agentpool/tool_impls/bash/tool.py +171 -0
  130. agentpool/tool_impls/delete_path/__init__.py +70 -0
  131. agentpool/tool_impls/delete_path/tool.py +142 -0
  132. agentpool/tool_impls/download_file/__init__.py +80 -0
  133. agentpool/tool_impls/download_file/tool.py +183 -0
  134. agentpool/tool_impls/execute_code/__init__.py +55 -0
  135. agentpool/tool_impls/execute_code/tool.py +163 -0
  136. agentpool/tool_impls/grep/__init__.py +80 -0
  137. agentpool/tool_impls/grep/tool.py +200 -0
  138. agentpool/tool_impls/list_directory/__init__.py +73 -0
  139. agentpool/tool_impls/list_directory/tool.py +197 -0
  140. agentpool/tool_impls/question/__init__.py +42 -0
  141. agentpool/tool_impls/question/tool.py +127 -0
  142. agentpool/tool_impls/read/__init__.py +104 -0
  143. agentpool/tool_impls/read/tool.py +305 -0
  144. agentpool/tools/__init__.py +2 -1
  145. agentpool/tools/base.py +114 -34
  146. agentpool/tools/manager.py +57 -1
  147. agentpool/ui/base.py +2 -2
  148. agentpool/ui/mock_provider.py +2 -2
  149. agentpool/ui/stdlib_provider.py +2 -2
  150. agentpool/utils/file_watcher.py +269 -0
  151. agentpool/utils/identifiers.py +121 -0
  152. agentpool/utils/pydantic_ai_helpers.py +46 -0
  153. agentpool/utils/streams.py +616 -2
  154. agentpool/utils/subprocess_utils.py +155 -0
  155. agentpool/utils/token_breakdown.py +461 -0
  156. agentpool/vfs_registry.py +7 -2
  157. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
  158. agentpool-2.5.0.dist-info/RECORD +579 -0
  159. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  160. agentpool_cli/__main__.py +24 -0
  161. agentpool_cli/create.py +1 -1
  162. agentpool_cli/serve_acp.py +100 -21
  163. agentpool_cli/serve_agui.py +87 -0
  164. agentpool_cli/serve_opencode.py +119 -0
  165. agentpool_cli/ui.py +557 -0
  166. agentpool_commands/__init__.py +42 -5
  167. agentpool_commands/agents.py +75 -2
  168. agentpool_commands/history.py +62 -0
  169. agentpool_commands/mcp.py +176 -0
  170. agentpool_commands/models.py +56 -3
  171. agentpool_commands/pool.py +260 -0
  172. agentpool_commands/session.py +1 -1
  173. agentpool_commands/text_sharing/__init__.py +119 -0
  174. agentpool_commands/text_sharing/base.py +123 -0
  175. agentpool_commands/text_sharing/github_gist.py +80 -0
  176. agentpool_commands/text_sharing/opencode.py +462 -0
  177. agentpool_commands/text_sharing/paste_rs.py +59 -0
  178. agentpool_commands/text_sharing/pastebin.py +116 -0
  179. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  180. agentpool_commands/tools.py +57 -0
  181. agentpool_commands/utils.py +80 -30
  182. agentpool_config/__init__.py +30 -2
  183. agentpool_config/agentpool_tools.py +498 -0
  184. agentpool_config/builtin_tools.py +77 -22
  185. agentpool_config/commands.py +24 -1
  186. agentpool_config/compaction.py +258 -0
  187. agentpool_config/converters.py +1 -1
  188. agentpool_config/event_handlers.py +42 -0
  189. agentpool_config/events.py +1 -1
  190. agentpool_config/forward_targets.py +1 -4
  191. agentpool_config/jinja.py +3 -3
  192. agentpool_config/mcp_server.py +132 -6
  193. agentpool_config/nodes.py +1 -1
  194. agentpool_config/observability.py +44 -0
  195. agentpool_config/session.py +0 -3
  196. agentpool_config/storage.py +82 -38
  197. agentpool_config/task.py +3 -3
  198. agentpool_config/tools.py +11 -22
  199. agentpool_config/toolsets.py +109 -233
  200. agentpool_server/a2a_server/agent_worker.py +307 -0
  201. agentpool_server/a2a_server/server.py +23 -18
  202. agentpool_server/acp_server/acp_agent.py +234 -181
  203. agentpool_server/acp_server/commands/acp_commands.py +151 -156
  204. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
  205. agentpool_server/acp_server/event_converter.py +651 -0
  206. agentpool_server/acp_server/input_provider.py +53 -10
  207. agentpool_server/acp_server/server.py +24 -90
  208. agentpool_server/acp_server/session.py +173 -331
  209. agentpool_server/acp_server/session_manager.py +8 -34
  210. agentpool_server/agui_server/server.py +3 -1
  211. agentpool_server/mcp_server/server.py +5 -2
  212. agentpool_server/opencode_server/.rules +95 -0
  213. agentpool_server/opencode_server/ENDPOINTS.md +401 -0
  214. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  215. agentpool_server/opencode_server/__init__.py +19 -0
  216. agentpool_server/opencode_server/command_validation.py +172 -0
  217. agentpool_server/opencode_server/converters.py +975 -0
  218. agentpool_server/opencode_server/dependencies.py +24 -0
  219. agentpool_server/opencode_server/input_provider.py +421 -0
  220. agentpool_server/opencode_server/models/__init__.py +250 -0
  221. agentpool_server/opencode_server/models/agent.py +53 -0
  222. agentpool_server/opencode_server/models/app.py +72 -0
  223. agentpool_server/opencode_server/models/base.py +26 -0
  224. agentpool_server/opencode_server/models/common.py +23 -0
  225. agentpool_server/opencode_server/models/config.py +37 -0
  226. agentpool_server/opencode_server/models/events.py +821 -0
  227. agentpool_server/opencode_server/models/file.py +88 -0
  228. agentpool_server/opencode_server/models/mcp.py +44 -0
  229. agentpool_server/opencode_server/models/message.py +179 -0
  230. agentpool_server/opencode_server/models/parts.py +323 -0
  231. agentpool_server/opencode_server/models/provider.py +81 -0
  232. agentpool_server/opencode_server/models/pty.py +43 -0
  233. agentpool_server/opencode_server/models/question.py +56 -0
  234. agentpool_server/opencode_server/models/session.py +111 -0
  235. agentpool_server/opencode_server/routes/__init__.py +29 -0
  236. agentpool_server/opencode_server/routes/agent_routes.py +473 -0
  237. agentpool_server/opencode_server/routes/app_routes.py +202 -0
  238. agentpool_server/opencode_server/routes/config_routes.py +302 -0
  239. agentpool_server/opencode_server/routes/file_routes.py +571 -0
  240. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  241. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  242. agentpool_server/opencode_server/routes/message_routes.py +761 -0
  243. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  244. agentpool_server/opencode_server/routes/pty_routes.py +300 -0
  245. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  246. agentpool_server/opencode_server/routes/session_routes.py +1276 -0
  247. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  248. agentpool_server/opencode_server/server.py +475 -0
  249. agentpool_server/opencode_server/state.py +151 -0
  250. agentpool_server/opencode_server/time_utils.py +8 -0
  251. agentpool_storage/__init__.py +12 -0
  252. agentpool_storage/base.py +184 -2
  253. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  254. agentpool_storage/claude_provider/__init__.py +42 -0
  255. agentpool_storage/claude_provider/provider.py +1089 -0
  256. agentpool_storage/file_provider.py +278 -15
  257. agentpool_storage/memory_provider.py +193 -12
  258. agentpool_storage/models.py +3 -0
  259. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  260. agentpool_storage/opencode_provider/__init__.py +16 -0
  261. agentpool_storage/opencode_provider/helpers.py +414 -0
  262. agentpool_storage/opencode_provider/provider.py +895 -0
  263. agentpool_storage/project_store.py +325 -0
  264. agentpool_storage/session_store.py +26 -6
  265. agentpool_storage/sql_provider/__init__.py +4 -2
  266. agentpool_storage/sql_provider/models.py +48 -0
  267. agentpool_storage/sql_provider/sql_provider.py +269 -3
  268. agentpool_storage/sql_provider/utils.py +12 -13
  269. agentpool_storage/zed_provider/__init__.py +16 -0
  270. agentpool_storage/zed_provider/helpers.py +281 -0
  271. agentpool_storage/zed_provider/models.py +130 -0
  272. agentpool_storage/zed_provider/provider.py +442 -0
  273. agentpool_storage/zed_provider.py +803 -0
  274. agentpool_toolsets/__init__.py +0 -2
  275. agentpool_toolsets/builtin/__init__.py +2 -12
  276. agentpool_toolsets/builtin/code.py +96 -57
  277. agentpool_toolsets/builtin/debug.py +118 -48
  278. agentpool_toolsets/builtin/execution_environment.py +115 -230
  279. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  280. agentpool_toolsets/builtin/skills.py +9 -4
  281. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  282. agentpool_toolsets/builtin/workers.py +4 -2
  283. agentpool_toolsets/composio_toolset.py +2 -2
  284. agentpool_toolsets/entry_points.py +3 -1
  285. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  286. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  287. agentpool_toolsets/fsspec_toolset/grep.py +99 -7
  288. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  289. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  290. agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
  291. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  292. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  293. agentpool_toolsets/mcp_discovery/toolset.py +511 -0
  294. agentpool_toolsets/mcp_run_toolset.py +87 -12
  295. agentpool_toolsets/notifications.py +33 -33
  296. agentpool_toolsets/openapi.py +3 -1
  297. agentpool_toolsets/search_toolset.py +3 -1
  298. agentpool-2.1.9.dist-info/RECORD +0 -474
  299. agentpool_config/resources.py +0 -33
  300. agentpool_server/acp_server/acp_tools.py +0 -43
  301. agentpool_server/acp_server/commands/spawn.py +0 -210
  302. agentpool_storage/text_log_provider.py +0 -275
  303. agentpool_toolsets/builtin/agent_management.py +0 -239
  304. agentpool_toolsets/builtin/chain.py +0 -288
  305. agentpool_toolsets/builtin/history.py +0 -36
  306. agentpool_toolsets/builtin/integration.py +0 -85
  307. agentpool_toolsets/builtin/tool_management.py +0 -90
  308. agentpool_toolsets/builtin/user_interaction.py +0 -52
  309. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  310. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  311. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,473 @@
1
+ """Agent, command, MCP, LSP, formatter, and logging routes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter, HTTPException
8
+ import httpx
9
+ from llmling_models.auth.anthropic_auth import (
10
+ AnthropicTokenStore,
11
+ build_authorization_url,
12
+ exchange_code_for_token,
13
+ generate_pkce,
14
+ )
15
+ from pydantic import BaseModel, HttpUrl
16
+
17
+ from agentpool.mcp_server.manager import MCPManager
18
+ from agentpool.resource_providers import AggregatingResourceProvider
19
+ from agentpool_config.mcp_server import (
20
+ SSEMCPServerConfig,
21
+ StdioMCPServerConfig,
22
+ StreamableHTTPMCPServerConfig,
23
+ )
24
+ from agentpool_server.opencode_server.dependencies import StateDep
25
+ from agentpool_server.opencode_server.models import (
26
+ Agent,
27
+ Command,
28
+ LogRequest,
29
+ McpResource,
30
+ MCPStatus,
31
+ )
32
+
33
+
34
+ router = APIRouter(tags=["agent"])
35
+
36
+
37
+ @router.get("/agent")
38
+ async def list_agents(state: StateDep) -> list[Agent]:
39
+ """List available agents from the AgentPool.
40
+
41
+ Returns all agents with their configurations, suitable for the agent
42
+ switcher UI. Agents are marked as primary (visible in switcher) or
43
+ subagent (hidden, used internally).
44
+ """
45
+ if state.agent.agent_pool is None:
46
+ return [
47
+ Agent(
48
+ name="default",
49
+ description="Default AgentPool agent",
50
+ mode="primary",
51
+ default=True,
52
+ )
53
+ ]
54
+
55
+ pool = state.agent.agent_pool
56
+ agents: list[Agent] = []
57
+ first_agent_name = next(iter(pool.all_agents.keys()), None)
58
+
59
+ for name, agent in pool.all_agents.items():
60
+ # Get description from agent
61
+ agents.append(
62
+ Agent(
63
+ name=name,
64
+ description=agent.description or f"Agent: {name}",
65
+ # model=AgentModel(model_id=agent.model_name or "unknown", provider_id=""),
66
+ mode="primary", # All agents visible for now; add hidden config later
67
+ default=(name == first_agent_name), # First agent is default
68
+ )
69
+ )
70
+
71
+ return (
72
+ agents
73
+ if agents
74
+ else [Agent(name="default", description="Default agent", mode="primary", default=True)]
75
+ )
76
+
77
+
78
+ @router.get("/command")
79
+ async def list_commands(state: StateDep) -> list[Command]:
80
+ """List available slash commands.
81
+
82
+ Commands are derived from MCP prompts available to the agent.
83
+ """
84
+ try:
85
+ prompts = await state.agent.tools.list_prompts()
86
+ return [Command(name=p.name, description=p.description or "") for p in prompts]
87
+ except Exception: # noqa: BLE001
88
+ return []
89
+
90
+
91
+ @router.get("/mcp")
92
+ async def get_mcp_status(state: StateDep) -> dict[str, MCPStatus]:
93
+ """Get MCP server status.
94
+
95
+ Returns status for each connected MCP server.
96
+ """
97
+ # Use agent's get_mcp_server_info method which handles different agent types
98
+ server_info = state.agent.get_mcp_server_info()
99
+
100
+ # Convert MCPServerStatus dataclass to MCPStatus response model
101
+ return {
102
+ name: MCPStatus(
103
+ name=status.name,
104
+ status=status.status,
105
+ error=status.error,
106
+ )
107
+ for name, status in server_info.items()
108
+ }
109
+
110
+
111
+ class AddMCPServerRequest(BaseModel):
112
+ """Request to add an MCP server dynamically."""
113
+
114
+ command: str | None = None
115
+ """Command to run (for stdio servers)."""
116
+
117
+ args: list[str] | None = None
118
+ """Arguments for the command."""
119
+
120
+ url: str | None = None
121
+ """URL for HTTP/SSE servers."""
122
+
123
+ env: dict[str, str] | None = None
124
+ """Environment variables for the server."""
125
+
126
+
127
+ @router.post("/mcp")
128
+ async def add_mcp_server(request: AddMCPServerRequest, state: StateDep) -> MCPStatus:
129
+ """Add an MCP server dynamically.
130
+
131
+ Supports stdio servers (command + args) or HTTP/SSE servers (url).
132
+ """
133
+ # Build the config based on request
134
+ # Note: client_id is auto-generated from command/url, custom names not supported
135
+ config: SSEMCPServerConfig | StdioMCPServerConfig | StreamableHTTPMCPServerConfig
136
+ if request.url:
137
+ # HTTP-based server
138
+ if request.url.endswith("/sse"):
139
+ config = SSEMCPServerConfig(url=HttpUrl(request.url))
140
+ else:
141
+ config = StreamableHTTPMCPServerConfig(url=HttpUrl(request.url))
142
+ elif request.command: # Stdio server
143
+ args = request.args or []
144
+ config = StdioMCPServerConfig(command=request.command, args=args, env=request.env)
145
+ else:
146
+ detail = "Must provide either 'command' (for stdio) or 'url' (for HTTP/SSE)"
147
+ raise HTTPException(status_code=400, detail=detail)
148
+
149
+ # Find the MCPManager and add the server
150
+ manager: MCPManager | None = None
151
+ for provider in state.agent.tools.external_providers:
152
+ if isinstance(provider, MCPManager):
153
+ manager = provider
154
+ break
155
+ if isinstance(provider, AggregatingResourceProvider):
156
+ for nested in provider.providers:
157
+ if isinstance(nested, MCPManager):
158
+ manager = nested
159
+ break
160
+
161
+ if manager is None:
162
+ raise HTTPException(status_code=400, detail="No MCP manager available")
163
+
164
+ try:
165
+ await manager.setup_server(config, add_to_config=True)
166
+ return MCPStatus(name=config.client_id, status="connected")
167
+ except Exception as e:
168
+ raise HTTPException(status_code=500, detail=f"Failed to add MCP server: {e}") from e
169
+
170
+
171
+ @router.post("/log")
172
+ async def log(request: LogRequest, state: StateDep) -> bool:
173
+ """Write a log entry.
174
+
175
+ TODO: Integrate with proper logging.
176
+ """
177
+ _ = state # unused for now
178
+ print(f"[{request.level}] {request.service}: {request.message}")
179
+ return True
180
+
181
+
182
+ @router.get("/experimental/resource")
183
+ async def list_mcp_resources(state: StateDep) -> dict[str, McpResource]:
184
+ """Get all available MCP resources from connected servers.
185
+
186
+ Returns a dictionary mapping resource keys to McpResource objects.
187
+ Keys are formatted as "{client}:{resource_name}" for uniqueness.
188
+ """
189
+ try:
190
+ resources = await state.agent.tools.list_resources()
191
+ result: dict[str, McpResource] = {}
192
+
193
+ for resource in resources:
194
+ # Create unique key: sanitize client and resource names
195
+ client_name = (resource.client or "unknown").replace("/", "_")
196
+ resource_name = resource.name.replace("/", "_")
197
+ key = f"{client_name}:{resource_name}"
198
+
199
+ result[key] = McpResource(
200
+ name=resource.name,
201
+ uri=resource.uri,
202
+ description=resource.description,
203
+ mime_type=resource.mime_type,
204
+ client=resource.client or "unknown",
205
+ )
206
+ except Exception: # noqa: BLE001
207
+ return {}
208
+ else:
209
+ return result
210
+
211
+
212
+ @router.get("/experimental/tool/ids")
213
+ async def list_tool_ids(state: StateDep) -> list[str]:
214
+ """List all available tool IDs.
215
+
216
+ Returns a list of tool names that are available to the agent.
217
+ OpenCode expects: Array<string>
218
+ """
219
+ try:
220
+ tools = await state.agent.tools.get_tools()
221
+ return [tool.name for tool in tools]
222
+ except Exception: # noqa: BLE001
223
+ return []
224
+
225
+
226
+ class ToolListItem(BaseModel):
227
+ """Tool info matching OpenCode SDK ToolListItem type."""
228
+
229
+ id: str
230
+ description: str
231
+ parameters: dict[str, Any]
232
+
233
+
234
+ @router.get("/experimental/tool")
235
+ async def list_tools_with_schemas( # noqa: D417
236
+ state: StateDep,
237
+ provider: str | None = None,
238
+ model: str | None = None,
239
+ ) -> list[ToolListItem]:
240
+ """List tools with their JSON schemas.
241
+
242
+ Args:
243
+ provider: Optional provider filter (not used currently)
244
+ model: Optional model filter (not used currently)
245
+
246
+ Returns list of tools matching OpenCode's ToolListItem format:
247
+ - id: string
248
+ - description: string
249
+ - parameters: unknown (JSON schema)
250
+ """
251
+ _ = provider, model # Currently unused, for future filtering
252
+
253
+ try:
254
+ tools = await state.agent.tools.get_tools()
255
+ result = []
256
+ for tool in tools:
257
+ # Extract parameters schema from the OpenAI function schema
258
+ schema = tool.schema
259
+ params = schema.get("function", {}).get("parameters", {})
260
+ item = ToolListItem(id=tool.name, description=tool.description or "", parameters=params)
261
+ result.append(item)
262
+ except Exception: # noqa: BLE001
263
+ return []
264
+ else:
265
+ return result
266
+
267
+
268
+ @router.get("/lsp")
269
+ async def get_lsp_status(state: StateDep) -> list[dict[str, Any]]:
270
+ """Get LSP server status.
271
+
272
+ Returns status of all running LSP servers.
273
+ """
274
+ try:
275
+ lsp_manager = state.get_or_create_lsp_manager()
276
+ servers = []
277
+ for server_id, server_state in lsp_manager._servers.items():
278
+ # OpenCode TUI expects "connected" or "error" for status colors
279
+ status = "connected" if server_state.initialized else "error"
280
+ servers.append({
281
+ "id": server_id,
282
+ "name": server_id,
283
+ "status": status,
284
+ "language": server_state.language,
285
+ "root": server_state.root_uri, # TUI uses "root" not "rootUri"
286
+ })
287
+ except Exception: # noqa: BLE001
288
+ return []
289
+ else:
290
+ return servers
291
+
292
+
293
+ @router.get("/formatter")
294
+ async def get_formatter_status(state: StateDep) -> list[dict[str, Any]]:
295
+ """Get formatter status.
296
+
297
+ Returns empty list - formatters not supported yet.
298
+ """
299
+ _ = state
300
+ return []
301
+
302
+
303
+ @router.get("/provider/auth")
304
+ async def get_provider_auth(state: StateDep) -> dict[str, list[dict[str, Any]]]:
305
+ """Get provider authentication methods.
306
+
307
+ Returns available OAuth providers with their auth methods.
308
+ """
309
+ _ = state
310
+ return {
311
+ "anthropic": [
312
+ {
313
+ "type": "oauth",
314
+ "label": "Connect Claude Max/Pro",
315
+ "method": "code",
316
+ }
317
+ ],
318
+ "copilot": [
319
+ {
320
+ "type": "oauth",
321
+ "label": "Connect GitHub Copilot",
322
+ "method": "device_code",
323
+ }
324
+ ],
325
+ }
326
+
327
+
328
+ # Store for active OAuth flows (in production, use Redis or similar)
329
+ _oauth_flows: dict[str, dict[str, Any]] = {}
330
+
331
+
332
+ @router.post("/provider/{provider_id}/oauth/authorize")
333
+ async def oauth_authorize(provider_id: str, state: StateDep) -> dict[str, Any]:
334
+ """Start OAuth authorization flow for a provider.
335
+
336
+ Returns URL and instructions for the user to complete authorization.
337
+ """
338
+ _ = state
339
+
340
+ if provider_id == "anthropic":
341
+ verifier, challenge = generate_pkce()
342
+ auth_url = build_authorization_url(verifier, challenge)
343
+
344
+ # Store verifier for callback
345
+ _oauth_flows[f"anthropic:{verifier}"] = {"verifier": verifier}
346
+
347
+ return {
348
+ "url": auth_url,
349
+ "instructions": "Sign in with your Anthropic account and copy the authorization code",
350
+ "method": "code",
351
+ "state": verifier,
352
+ }
353
+
354
+ if provider_id == "copilot":
355
+ async with httpx.AsyncClient(timeout=30.0) as client:
356
+ resp = await client.post(
357
+ "https://github.com/login/device/code",
358
+ headers={
359
+ "accept": "application/json",
360
+ "editor-version": "Neovim/0.6.1",
361
+ "editor-plugin-version": "copilot.vim/1.16.0",
362
+ "content-type": "application/json",
363
+ "user-agent": "GithubCopilot/1.155.0",
364
+ },
365
+ json={"client_id": "Iv1.b507a08c87ecfe98", "scope": "read:user"},
366
+ )
367
+ resp.raise_for_status()
368
+ data = resp.json()
369
+
370
+ device_code = data["device_code"]
371
+ user_code = data["user_code"]
372
+ verification_uri = data["verification_uri"]
373
+
374
+ # Store device_code for callback
375
+ _oauth_flows[f"copilot:{device_code}"] = {"device_code": device_code}
376
+
377
+ return {
378
+ "url": verification_uri,
379
+ "instructions": f"Enter code: {user_code}",
380
+ "method": "device_code",
381
+ "user_code": user_code,
382
+ "device_code": device_code,
383
+ }
384
+
385
+ raise HTTPException(status_code=404, detail=f"Unknown provider: {provider_id}")
386
+
387
+
388
+ @router.post("/provider/{provider_id}/oauth/callback")
389
+ async def oauth_callback(
390
+ provider_id: str,
391
+ state: StateDep,
392
+ code: str | None = None,
393
+ device_code: str | None = None,
394
+ verifier: str | None = None,
395
+ ) -> dict[str, Any]:
396
+ """Handle OAuth callback/code exchange.
397
+
398
+ For Anthropic: exchanges authorization code for tokens.
399
+ For Copilot: polls for token using device code.
400
+ """
401
+ _ = state
402
+
403
+ if provider_id == "anthropic":
404
+ if not code or not verifier:
405
+ raise HTTPException(
406
+ status_code=400, detail="Missing code or verifier for Anthropic OAuth"
407
+ )
408
+
409
+ try:
410
+ token = exchange_code_for_token(code, verifier)
411
+ # Save token
412
+ store = AnthropicTokenStore()
413
+ store.save(token)
414
+
415
+ # Clean up flow state
416
+ _oauth_flows.pop(f"anthropic:{verifier}", None)
417
+ except Exception as e:
418
+ raise HTTPException(status_code=400, detail=str(e)) from e
419
+ else:
420
+ return {
421
+ "type": "success",
422
+ "access": token.access_token,
423
+ "refresh": token.refresh_token,
424
+ "expires": token.expires_at,
425
+ }
426
+
427
+ if provider_id == "copilot":
428
+ if not device_code:
429
+ raise HTTPException(
430
+ status_code=400,
431
+ detail="Missing device_code for Copilot OAuth",
432
+ )
433
+
434
+ async with httpx.AsyncClient(timeout=30.0) as client:
435
+ resp = await client.post(
436
+ "https://github.com/login/oauth/access_token",
437
+ headers={
438
+ "accept": "application/json",
439
+ "editor-version": "Neovim/0.6.1",
440
+ "editor-plugin-version": "copilot.vim/1.16.0",
441
+ "content-type": "application/json",
442
+ "user-agent": "GithubCopilot/1.155.0",
443
+ },
444
+ json={
445
+ "client_id": "Iv1.b507a08c87ecfe98",
446
+ "device_code": device_code,
447
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
448
+ },
449
+ )
450
+ data = resp.json()
451
+
452
+ if "error" in data:
453
+ if data["error"] == "authorization_pending":
454
+ return {"type": "pending", "message": "Waiting for user authorization"}
455
+ raise HTTPException(
456
+ status_code=400,
457
+ detail=data.get("error_description", data["error"]),
458
+ )
459
+
460
+ access_token = data.get("access_token")
461
+ if access_token:
462
+ # Clean up flow state
463
+ _oauth_flows.pop(f"copilot:{device_code}", None)
464
+
465
+ return {
466
+ "type": "success",
467
+ "access": access_token,
468
+ "refresh": data.get("refresh_token"),
469
+ "expires": None, # Copilot tokens don't expire the same way
470
+ }
471
+
472
+ return {"type": "pending", "message": "No token received yet"}
473
+ raise HTTPException(status_code=404, detail=f"Unknown provider: {provider_id}")
@@ -0,0 +1,202 @@
1
+ """App, project, path, and VCS routes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import subprocess
7
+ from typing import TYPE_CHECKING
8
+
9
+ from fastapi import APIRouter, HTTPException
10
+
11
+ from agentpool_server.opencode_server.dependencies import StateDep
12
+ from agentpool_server.opencode_server.models import (
13
+ App,
14
+ AppTimeInfo,
15
+ PathInfo,
16
+ Project,
17
+ ProjectTime,
18
+ ProjectUpdatedEvent,
19
+ ProjectUpdateRequest,
20
+ VcsInfo,
21
+ )
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from agentpool.sessions.models import ProjectData
26
+
27
+
28
+ router = APIRouter(tags=["app"])
29
+
30
+
31
+ @router.get("/app")
32
+ async def get_app(state: StateDep) -> App:
33
+ """Get app information."""
34
+ working_path = Path(state.working_dir)
35
+ return App(
36
+ git=(working_path / ".git").is_dir(),
37
+ hostname="localhost",
38
+ path=PathInfo(
39
+ config="",
40
+ cwd=state.working_dir,
41
+ data="",
42
+ root=state.working_dir,
43
+ state="",
44
+ ),
45
+ time=AppTimeInfo(initialized=state.start_time),
46
+ )
47
+
48
+
49
+ def _project_data_to_response(data: ProjectData) -> Project:
50
+ """Convert ProjectData to OpenCode Project response."""
51
+ working_path = Path(data.worktree)
52
+ vcs_dir: str | None = None
53
+ if data.vcs == "git":
54
+ vcs_dir = str(working_path / ".git")
55
+ elif data.vcs == "hg":
56
+ vcs_dir = str(working_path / ".hg")
57
+
58
+ return Project(
59
+ id=data.project_id,
60
+ worktree=data.worktree,
61
+ vcs_dir=vcs_dir,
62
+ vcs=data.vcs,
63
+ time=ProjectTime(created=int(data.created_at.timestamp() * 1000)),
64
+ )
65
+
66
+
67
+ async def _get_current_project(state: StateDep) -> ProjectData:
68
+ """Get or create the current project from storage."""
69
+ from agentpool_storage.project_store import ProjectStore
70
+
71
+ storage = state.pool.storage
72
+ project_store = ProjectStore(storage)
73
+ return await project_store.get_or_create(state.working_dir)
74
+
75
+
76
+ @router.get("/project")
77
+ async def list_projects(state: StateDep) -> list[Project]:
78
+ """List all projects."""
79
+ from agentpool_storage.project_store import ProjectStore
80
+
81
+ storage = state.pool.storage
82
+ project_store = ProjectStore(storage)
83
+ projects = await project_store.list_recent(limit=50)
84
+ return [_project_data_to_response(p) for p in projects]
85
+
86
+
87
+ @router.get("/project/current")
88
+ async def get_project_current(state: StateDep) -> Project:
89
+ """Get current project."""
90
+ project = await _get_current_project(state)
91
+ return _project_data_to_response(project)
92
+
93
+
94
+ @router.patch("/project/{project_id}")
95
+ async def update_project(
96
+ project_id: str,
97
+ update: ProjectUpdateRequest,
98
+ state: StateDep,
99
+ ) -> Project:
100
+ """Update project metadata (name, settings).
101
+
102
+ Emits a project.updated event when successful.
103
+
104
+ Args:
105
+ project_id: Project identifier
106
+ update: Fields to update (name and/or settings)
107
+ state: Server state
108
+
109
+ Returns:
110
+ Updated project data
111
+
112
+ Raises:
113
+ HTTPException: If project not found
114
+ """
115
+ from agentpool_storage.project_store import ProjectStore
116
+
117
+ store = ProjectStore(state.pool.storage)
118
+ project_data = None
119
+
120
+ # Update name if provided
121
+ if update.name is not None:
122
+ project_data = await store.set_name(project_id, update.name)
123
+ if not project_data:
124
+ raise HTTPException(status_code=404, detail="Project not found")
125
+
126
+ # Update settings if provided
127
+ if update.settings:
128
+ if project_data:
129
+ # Already fetched from set_name, update with settings
130
+ project_data = await store.update_settings(project_id, **update.settings)
131
+ else:
132
+ project_data = await store.update_settings(project_id, **update.settings)
133
+
134
+ if not project_data:
135
+ raise HTTPException(status_code=404, detail="Project not found")
136
+
137
+ # If neither name nor settings provided, just fetch the project
138
+ if not project_data:
139
+ project_data = await store.get_by_id(project_id)
140
+ if not project_data:
141
+ raise HTTPException(status_code=404, detail="Project not found")
142
+
143
+ # Convert to OpenCode Project model
144
+ project = _project_data_to_response(project_data)
145
+
146
+ # Broadcast event
147
+ await state.broadcast_event(ProjectUpdatedEvent.create(project))
148
+
149
+ return project
150
+
151
+
152
+ @router.get("/path")
153
+ async def get_path(state: StateDep) -> PathInfo:
154
+ """Get current path info."""
155
+ return PathInfo(
156
+ config="",
157
+ cwd=state.working_dir,
158
+ data="",
159
+ root=state.working_dir,
160
+ state="",
161
+ )
162
+
163
+
164
+ @router.get("/vcs")
165
+ async def get_vcs(state: StateDep) -> VcsInfo:
166
+ """Get VCS info.
167
+
168
+ TODO: For remote/ACP support, these git commands should run through
169
+ state.env.execute_command() instead of subprocess.run() so they
170
+ execute on the client side where the repository lives.
171
+ """
172
+ git_dir = Path(state.working_dir) / ".git"
173
+ if not git_dir.is_dir():
174
+ return VcsInfo(branch=None, dirty=False, commit=None)
175
+
176
+ try:
177
+ branch = subprocess.run(
178
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
179
+ cwd=state.working_dir,
180
+ capture_output=True,
181
+ text=True,
182
+ check=True,
183
+ ).stdout.strip()
184
+ commit = subprocess.run(
185
+ ["git", "rev-parse", "HEAD"],
186
+ cwd=state.working_dir,
187
+ capture_output=True,
188
+ text=True,
189
+ check=True,
190
+ ).stdout.strip()
191
+ dirty = bool(
192
+ subprocess.run(
193
+ ["git", "status", "--porcelain"],
194
+ cwd=state.working_dir,
195
+ capture_output=True,
196
+ text=True,
197
+ check=True,
198
+ ).stdout.strip()
199
+ )
200
+ return VcsInfo(branch=branch, dirty=dirty, commit=commit)
201
+ except subprocess.CalledProcessError:
202
+ return VcsInfo(branch=None, dirty=False, commit=None)