illusion-code 0.1.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 (214) hide show
  1. illusion/__init__.py +24 -0
  2. illusion/__main__.py +15 -0
  3. illusion/_frontend/dist/index.mjs +39208 -0
  4. illusion/_frontend/package.json +27 -0
  5. illusion/_frontend/src/App.tsx +624 -0
  6. illusion/_frontend/src/components/CommandPicker.tsx +98 -0
  7. illusion/_frontend/src/components/Composer.tsx +55 -0
  8. illusion/_frontend/src/components/ComposerController.tsx +128 -0
  9. illusion/_frontend/src/components/ConversationView.tsx +750 -0
  10. illusion/_frontend/src/components/Footer.tsx +25 -0
  11. illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
  12. illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
  13. illusion/_frontend/src/components/ModalHost.tsx +425 -0
  14. illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
  15. illusion/_frontend/src/components/PromptInput.tsx +64 -0
  16. illusion/_frontend/src/components/SelectModal.tsx +78 -0
  17. illusion/_frontend/src/components/SidePanel.tsx +175 -0
  18. illusion/_frontend/src/components/Spinner.tsx +77 -0
  19. illusion/_frontend/src/components/StatusBar.tsx +142 -0
  20. illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
  21. illusion/_frontend/src/components/TodoPanel.tsx +126 -0
  22. illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
  23. illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
  24. illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
  25. illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
  26. illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
  27. illusion/_frontend/src/i18n.ts +78 -0
  28. illusion/_frontend/src/index.tsx +42 -0
  29. illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
  30. illusion/_frontend/src/theme/builtinThemes.ts +89 -0
  31. illusion/_frontend/src/types.ts +110 -0
  32. illusion/_frontend/src/utils/markdown.ts +33 -0
  33. illusion/_frontend/src/utils/thinking.ts +191 -0
  34. illusion/_frontend/tsconfig.json +13 -0
  35. illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
  36. illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
  37. illusion/_web_dist/index.html +16 -0
  38. illusion/api/__init__.py +36 -0
  39. illusion/api/client.py +568 -0
  40. illusion/api/codex_client.py +563 -0
  41. illusion/api/compat.py +138 -0
  42. illusion/api/effort.py +128 -0
  43. illusion/api/errors.py +57 -0
  44. illusion/api/openai_client.py +819 -0
  45. illusion/api/provider.py +148 -0
  46. illusion/api/registry.py +479 -0
  47. illusion/api/usage.py +45 -0
  48. illusion/auth/__init__.py +50 -0
  49. illusion/auth/copilot.py +419 -0
  50. illusion/auth/external.py +612 -0
  51. illusion/auth/flows.py +58 -0
  52. illusion/auth/manager.py +214 -0
  53. illusion/auth/storage.py +372 -0
  54. illusion/bridge/__init__.py +38 -0
  55. illusion/bridge/manager.py +190 -0
  56. illusion/bridge/session_runner.py +84 -0
  57. illusion/bridge/types.py +113 -0
  58. illusion/bridge/work_secret.py +131 -0
  59. illusion/cli.py +1228 -0
  60. illusion/commands/__init__.py +32 -0
  61. illusion/commands/registry.py +1934 -0
  62. illusion/config/__init__.py +39 -0
  63. illusion/config/i18n.py +522 -0
  64. illusion/config/paths.py +259 -0
  65. illusion/config/settings.py +564 -0
  66. illusion/coordinator/__init__.py +41 -0
  67. illusion/coordinator/agent_definitions.py +1093 -0
  68. illusion/coordinator/coordinator_mode.py +127 -0
  69. illusion/engine/__init__.py +95 -0
  70. illusion/engine/cost_tracker.py +55 -0
  71. illusion/engine/messages.py +369 -0
  72. illusion/engine/query.py +632 -0
  73. illusion/engine/query_engine.py +343 -0
  74. illusion/engine/stream_events.py +169 -0
  75. illusion/hooks/__init__.py +67 -0
  76. illusion/hooks/events.py +43 -0
  77. illusion/hooks/executor.py +397 -0
  78. illusion/hooks/hot_reload.py +74 -0
  79. illusion/hooks/loader.py +133 -0
  80. illusion/hooks/schemas.py +121 -0
  81. illusion/hooks/types.py +86 -0
  82. illusion/mcp/__init__.py +104 -0
  83. illusion/mcp/client.py +377 -0
  84. illusion/mcp/config.py +140 -0
  85. illusion/mcp/types.py +175 -0
  86. illusion/memory/__init__.py +36 -0
  87. illusion/memory/manager.py +94 -0
  88. illusion/memory/memdir.py +58 -0
  89. illusion/memory/paths.py +57 -0
  90. illusion/memory/scan.py +120 -0
  91. illusion/memory/search.py +83 -0
  92. illusion/memory/types.py +43 -0
  93. illusion/output_styles/__init__.py +15 -0
  94. illusion/output_styles/loader.py +64 -0
  95. illusion/permissions/__init__.py +39 -0
  96. illusion/permissions/checker.py +174 -0
  97. illusion/permissions/modes.py +38 -0
  98. illusion/platforms.py +148 -0
  99. illusion/plugins/__init__.py +71 -0
  100. illusion/plugins/bundled/__init__.py +0 -0
  101. illusion/plugins/installer.py +59 -0
  102. illusion/plugins/loader.py +301 -0
  103. illusion/plugins/schemas.py +51 -0
  104. illusion/plugins/types.py +56 -0
  105. illusion/prompts/__init__.py +29 -0
  106. illusion/prompts/claudemd.py +74 -0
  107. illusion/prompts/context.py +187 -0
  108. illusion/prompts/environment.py +189 -0
  109. illusion/prompts/system_prompt.py +155 -0
  110. illusion/py.typed +0 -0
  111. illusion/sandbox/__init__.py +29 -0
  112. illusion/sandbox/adapter.py +174 -0
  113. illusion/services/__init__.py +59 -0
  114. illusion/services/compact/__init__.py +1015 -0
  115. illusion/services/cron.py +338 -0
  116. illusion/services/cron_scheduler.py +715 -0
  117. illusion/services/file_history.py +258 -0
  118. illusion/services/lsp/__init__.py +455 -0
  119. illusion/services/session_storage.py +237 -0
  120. illusion/services/token_estimation.py +72 -0
  121. illusion/skills/__init__.py +60 -0
  122. illusion/skills/bundled/__init__.py +110 -0
  123. illusion/skills/bundled/content/batch.md +86 -0
  124. illusion/skills/bundled/content/coding-guidelines.md +70 -0
  125. illusion/skills/bundled/content/debug.md +38 -0
  126. illusion/skills/bundled/content/loop.md +82 -0
  127. illusion/skills/bundled/content/remember.md +105 -0
  128. illusion/skills/bundled/content/simplify.md +53 -0
  129. illusion/skills/bundled/content/skillify.md +113 -0
  130. illusion/skills/bundled/content/stuck.md +54 -0
  131. illusion/skills/bundled/content/update-config.md +329 -0
  132. illusion/skills/bundled/content/verify.md +74 -0
  133. illusion/skills/loader.py +219 -0
  134. illusion/skills/registry.py +40 -0
  135. illusion/skills/types.py +24 -0
  136. illusion/state/__init__.py +18 -0
  137. illusion/state/app_state.py +67 -0
  138. illusion/state/store.py +93 -0
  139. illusion/swarm/__init__.py +71 -0
  140. illusion/swarm/agent_executor.py +857 -0
  141. illusion/swarm/in_process.py +259 -0
  142. illusion/swarm/subprocess_backend.py +136 -0
  143. illusion/swarm/team_helpers.py +123 -0
  144. illusion/swarm/types.py +159 -0
  145. illusion/swarm/worktree.py +347 -0
  146. illusion/tasks/__init__.py +33 -0
  147. illusion/tasks/local_agent_task.py +42 -0
  148. illusion/tasks/local_shell_task.py +27 -0
  149. illusion/tasks/manager.py +377 -0
  150. illusion/tasks/stop_task.py +21 -0
  151. illusion/tasks/types.py +88 -0
  152. illusion/tools/__init__.py +126 -0
  153. illusion/tools/agent_tool.py +388 -0
  154. illusion/tools/ask_user_question_tool.py +186 -0
  155. illusion/tools/base.py +149 -0
  156. illusion/tools/bash_tool.py +413 -0
  157. illusion/tools/config_tool.py +90 -0
  158. illusion/tools/cron_tool.py +473 -0
  159. illusion/tools/enter_plan_mode_tool.py +147 -0
  160. illusion/tools/enter_worktree_tool.py +188 -0
  161. illusion/tools/exit_plan_mode_tool.py +69 -0
  162. illusion/tools/exit_worktree_tool.py +225 -0
  163. illusion/tools/file_edit_tool.py +283 -0
  164. illusion/tools/file_read_tool.py +294 -0
  165. illusion/tools/file_write_tool.py +184 -0
  166. illusion/tools/glob_tool.py +165 -0
  167. illusion/tools/grep_tool.py +190 -0
  168. illusion/tools/list_mcp_resources_tool.py +80 -0
  169. illusion/tools/lsp_tool.py +333 -0
  170. illusion/tools/mcp_auth_tool.py +100 -0
  171. illusion/tools/mcp_tool.py +75 -0
  172. illusion/tools/notebook_edit_tool.py +242 -0
  173. illusion/tools/powershell_tool.py +334 -0
  174. illusion/tools/read_mcp_resource_tool.py +63 -0
  175. illusion/tools/repl_tool.py +100 -0
  176. illusion/tools/send_message_tool.py +112 -0
  177. illusion/tools/shell_common.py +187 -0
  178. illusion/tools/skill_tool.py +86 -0
  179. illusion/tools/sleep_tool.py +62 -0
  180. illusion/tools/structured_output_tool.py +58 -0
  181. illusion/tools/task_create_tool.py +98 -0
  182. illusion/tools/task_get_tool.py +94 -0
  183. illusion/tools/task_list_tool.py +94 -0
  184. illusion/tools/task_output_tool.py +55 -0
  185. illusion/tools/task_stop_tool.py +52 -0
  186. illusion/tools/task_update_tool.py +224 -0
  187. illusion/tools/team_create_tool.py +236 -0
  188. illusion/tools/team_delete_tool.py +104 -0
  189. illusion/tools/todo_write_tool.py +198 -0
  190. illusion/tools/tool_search_tool.py +156 -0
  191. illusion/tools/web_fetch_tool.py +264 -0
  192. illusion/tools/web_search_tool.py +186 -0
  193. illusion/ui/__init__.py +23 -0
  194. illusion/ui/app.py +258 -0
  195. illusion/ui/backend_host.py +1180 -0
  196. illusion/ui/input.py +86 -0
  197. illusion/ui/output.py +363 -0
  198. illusion/ui/permission_dialog.py +47 -0
  199. illusion/ui/permission_store.py +99 -0
  200. illusion/ui/protocol.py +384 -0
  201. illusion/ui/react_launcher.py +280 -0
  202. illusion/ui/runtime.py +787 -0
  203. illusion/ui/textual_app.py +603 -0
  204. illusion/ui/web/__init__.py +10 -0
  205. illusion/ui/web/server.py +87 -0
  206. illusion/ui/web/ws_host.py +1197 -0
  207. illusion/utils/__init__.py +0 -0
  208. illusion/utils/ripgrep.py +299 -0
  209. illusion/utils/shell.py +248 -0
  210. illusion_code-0.1.0.dist-info/METADATA +1159 -0
  211. illusion_code-0.1.0.dist-info/RECORD +214 -0
  212. illusion_code-0.1.0.dist-info/WHEEL +4 -0
  213. illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
  214. illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,121 @@
1
+ """
2
+ 钩子配置模式定义
3
+ ================
4
+
5
+ 本模块定义钩子的配置数据模型,使用 Pydantic 进行验证。
6
+
7
+ 支持的钩子类型:
8
+ - CommandHookDefinition: 执行 Shell 命令的钩子
9
+ - PromptHookDefinition: 使用模型验证条件的钩子
10
+ - HttpHookDefinition: 发送 HTTP 请求的钩子
11
+ - AgentHookDefinition: 使用 Agent 进行深度验证的钩子
12
+
13
+ 使用示例:
14
+ >>> from illusion.hooks.schemas import CommandHookDefinition
15
+ >>> hook = CommandHookDefinition(type="command", command="echo hello")
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Literal
21
+
22
+ from pydantic import BaseModel, Field
23
+
24
+
25
+ class CommandHookDefinition(BaseModel):
26
+ """
27
+ 命令钩子定义
28
+
29
+ 执行 Shell 命令的钩子类型。
30
+
31
+ Attributes:
32
+ type: 钩子类型,固定为 "command"
33
+ command: 要执行的 shell 命令
34
+ timeout_seconds: 超时时间(秒),默认30秒,范围1-600
35
+ matcher: 可选的匹配器,用于过滤 payload
36
+ block_on_failure: 失败时是否阻止后续操作,默认 False
37
+ """
38
+
39
+ type: Literal["command"] = "command" # 钩子类型标识
40
+ command: str # 要执行的命令
41
+ timeout_seconds: int = Field(default=30, ge=1, le=600) # 超时时间(秒)
42
+ matcher: str | None = None # payload 匹配器
43
+ block_on_failure: bool = False # 失败时是否阻塞
44
+
45
+
46
+ class PromptHookDefinition(BaseModel):
47
+ """
48
+ 提示词钩子定义
49
+
50
+ 使用大语言模型验证条件的钩子类型。
51
+
52
+ Attributes:
53
+ type: 钩子类型,固定为 "prompt"
54
+ prompt: 验证提示词
55
+ model: 可选的模型名称,默认使用上下文中的模型
56
+ timeout_seconds: 超时时间(秒),默认30秒,范围1-600
57
+ matcher: 可选的匹配器,用于过滤 payload
58
+ block_on_failure: 失败时是否阻止后续操作,默认 True
59
+ """
60
+
61
+ type: Literal["prompt"] = "prompt" # 钩子类型标识
62
+ prompt: str # 验证提示词
63
+ model: str | None = None # 可选的模型名称
64
+ timeout_seconds: int = Field(default=30, ge=1, le=600) # 超时时间(秒)
65
+ matcher: str | None = None # payload 匹配器
66
+ block_on_failure: bool = True # 失败时是否阻塞
67
+
68
+
69
+ class HttpHookDefinition(BaseModel):
70
+ """
71
+ HTTP 钩子定义
72
+
73
+ 向 HTTP 端点发送事件载荷的钩子类型。
74
+
75
+ Attributes:
76
+ type: 钩子类型,固定为 "http"
77
+ url: 请求目标 URL
78
+ headers: 可选的请求头字典
79
+ timeout_seconds: 超时时间(秒),默认30秒,范围1-600
80
+ matcher: 可选的匹配器,用于过滤 payload
81
+ block_on_failure: 失败时是否阻止后续操作,默认 False
82
+ """
83
+
84
+ type: Literal["http"] = "http" # 钩子类型标识
85
+ url: str # 请求 URL
86
+ headers: dict[str, str] = Field(default_factory=dict) # 请求头
87
+ timeout_seconds: int = Field(default=30, ge=1, le=600) # 超时时间(秒)
88
+ matcher: str | None = None # payload 匹配器
89
+ block_on_failure: bool = False # 失败时是否阻塞
90
+
91
+
92
+ class AgentHookDefinition(BaseModel):
93
+ """
94
+ Agent 钩子定义
95
+
96
+ 使用 Agent 进行深度模型验证的钩子类型。
97
+
98
+ Attributes:
99
+ type: 钩子类型,固定为 "agent"
100
+ prompt: 验证提示词
101
+ model: 可选的模型名称,默认使用上下文中的模型
102
+ timeout_seconds: 超时时间(秒),默认60秒,范围1-1200
103
+ matcher: 可选的匹配器,用于过滤 payload
104
+ block_on_failure: 失败时是否阻止后续操作,默认 True
105
+ """
106
+
107
+ type: Literal["agent"] = "agent" # 钩子类型标识
108
+ prompt: str # 验证提示词
109
+ model: str | None = None # 可选的模型名称
110
+ timeout_seconds: int = Field(default=60, ge=1, le=1200) # 超时时间(秒)
111
+ matcher: str | None = None # payload 匹配器
112
+ block_on_failure: bool = True # 失败时是否阻塞
113
+
114
+
115
+ # 联合类型:所有钩子定义的联合
116
+ HookDefinition = (
117
+ CommandHookDefinition
118
+ | PromptHookDefinition
119
+ | HttpHookDefinition
120
+ | AgentHookDefinition
121
+ )
@@ -0,0 +1,86 @@
1
+ """
2
+ 运行时钩子结果类型
3
+ ==================
4
+
5
+ 本模块定义钩子执行后的结果类型,包括单个钩子结果和聚合结果。
6
+
7
+ 主要类型:
8
+ - HookResult: 单个钩子的执行结果
9
+ - AggregatedHookResult: 多个钩子结果的聚合
10
+
11
+ 使用示例:
12
+ >>> from illusion.hooks.types import HookResult, AggregatedHookResult
13
+ >>> result = HookResult(hook_type="command", success=True, output="done")
14
+ >>> aggregated = AggregatedHookResult(results=[result])
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass, field
20
+ from typing import Any
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class HookResult:
25
+ """
26
+ 单个钩子执行结果
27
+
28
+ 存储一个钩子执行后的状态和输出信息。
29
+
30
+ Attributes:
31
+ hook_type: 钩子类型(command/prompt/http/agent)
32
+ success: 是否成功执行
33
+ output: 钩子输出内容
34
+ blocked: 是否阻止继续执行
35
+ reason: 阻止原因(当 blocked 为 True 时)
36
+ metadata: 附加元数据字典
37
+ """
38
+
39
+ hook_type: str # 钩子类型标识
40
+ success: bool # 执行是否成功
41
+ output: str = "" # 钩子输出内容
42
+ blocked: bool = False # 是否阻止后续操作
43
+ reason: str = "" # 阻止原因描述
44
+ metadata: dict[str, Any] = field(default_factory=dict) # 额外元数据
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class AggregatedHookResult:
49
+ """
50
+ 聚合钩子结果
51
+
52
+ 聚合多个钩子的执行结果,提供统一的 blocked 和 reason 属性。
53
+
54
+ Attributes:
55
+ results: 钩子结果列表
56
+
57
+ 使用示例:
58
+ >>> aggregated = AggregatedHookResult(results=[result1, result2])
59
+ >>> if aggregated.blocked:
60
+ ... print(aggregated.reason)
61
+ """
62
+
63
+ results: list[HookResult] = field(default_factory=list) # 钩子结果列表
64
+
65
+ @property
66
+ def blocked(self) -> bool:
67
+ """
68
+ 检查是否有任何钩子阻止继续执行
69
+
70
+ Returns:
71
+ bool: 如果任意一个钩子 blocked 为 True,返回 True
72
+ """
73
+ return any(result.blocked for result in self.results)
74
+
75
+ @property
76
+ def reason(self) -> str:
77
+ """
78
+ 获取第一个阻止原因
79
+
80
+ Returns:
81
+ str: 第一个 blocked=True 的钩子的 reason,如果没有则返回空字符串
82
+ """
83
+ for result in self.results:
84
+ if result.blocked:
85
+ return result.reason or result.output
86
+ return ""
@@ -0,0 +1,104 @@
1
+ """
2
+ MCP 模块
3
+ ========
4
+
5
+ 本模块提供 MCP(Model Context Protocol)客户端和管理功能。
6
+
7
+ 主要组件:
8
+ - McpClientManager: MCP 客户端管理器
9
+ - McpServerConfig: MCP 服务器配置
10
+ - McpStdioServerConfig: STDIO 服务器配置
11
+ - McpHttpServerConfig: HTTP 服务器配置
12
+ - McpWebSocketServerConfig: WebSocket 服务器配置
13
+ - McpToolInfo: MCP 工具信息
14
+ - McpResourceInfo: MCP 资源信息
15
+ - McpConnectionStatus: MCP 连接状态
16
+ - load_mcp_server_configs: 加载 MCP 服务器配置
17
+ - load_project_mcp_configs: 加载项目级 MCP 配置
18
+
19
+ 使用示例:
20
+ >>> from illusion.mcp import McpClientManager, load_mcp_server_configs
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import TYPE_CHECKING
26
+
27
+ # 类型检查时导入,避免循环依赖
28
+ if TYPE_CHECKING: # pragma: no cover
29
+ from illusion.mcp.client import McpClientManager
30
+ from illusion.mcp.types import (
31
+ McpConnectionStatus,
32
+ McpHttpServerConfig,
33
+ McpJsonConfig,
34
+ McpResourceInfo,
35
+ McpServerConfig,
36
+ McpStdioServerConfig,
37
+ McpToolInfo,
38
+ McpWebSocketServerConfig,
39
+ )
40
+
41
+ __all__ = [
42
+ "McpClientManager",
43
+ "McpConnectionStatus",
44
+ "McpHttpServerConfig",
45
+ "McpJsonConfig",
46
+ "McpResourceInfo",
47
+ "McpServerConfig",
48
+ "McpStdioServerConfig",
49
+ "McpToolInfo",
50
+ "McpWebSocketServerConfig",
51
+ "load_mcp_server_configs",
52
+ "load_project_mcp_configs",
53
+ ]
54
+
55
+
56
+ def __getattr__(name: str):
57
+ # 延迟导入 McpClientManager,避免不必要的导入开销
58
+ if name == "McpClientManager":
59
+ from illusion.mcp.client import McpClientManager
60
+
61
+ return McpClientManager
62
+ # 延迟导入 load_mcp_server_configs
63
+ if name == "load_mcp_server_configs":
64
+ from illusion.mcp.config import load_mcp_server_configs
65
+
66
+ return load_mcp_server_configs
67
+ # 延迟导入 load_project_mcp_configs
68
+ if name == "load_project_mcp_configs":
69
+ from illusion.mcp.config import load_project_mcp_configs
70
+
71
+ return load_project_mcp_configs
72
+ # 延迟导入类型定义
73
+ if name in {
74
+ "McpConnectionStatus",
75
+ "McpHttpServerConfig",
76
+ "McpJsonConfig",
77
+ "McpResourceInfo",
78
+ "McpServerConfig",
79
+ "McpStdioServerConfig",
80
+ "McpToolInfo",
81
+ "McpWebSocketServerConfig",
82
+ }:
83
+ from illusion.mcp.types import (
84
+ McpConnectionStatus,
85
+ McpHttpServerConfig,
86
+ McpJsonConfig,
87
+ McpResourceInfo,
88
+ McpServerConfig,
89
+ McpStdioServerConfig,
90
+ McpToolInfo,
91
+ McpWebSocketServerConfig,
92
+ )
93
+
94
+ return {
95
+ "McpConnectionStatus": McpConnectionStatus,
96
+ "McpHttpServerConfig": McpHttpServerConfig,
97
+ "McpJsonConfig": McpJsonConfig,
98
+ "McpResourceInfo": McpResourceInfo,
99
+ "McpServerConfig": McpServerConfig,
100
+ "McpStdioServerConfig": McpStdioServerConfig,
101
+ "McpToolInfo": McpToolInfo,
102
+ "McpWebSocketServerConfig": McpWebSocketServerConfig,
103
+ }[name]
104
+ raise AttributeError(name)
illusion/mcp/client.py ADDED
@@ -0,0 +1,377 @@
1
+ """
2
+ MCP 客户端管理器模块
3
+ ===================
4
+
5
+ 本模块提供 MCP(Model Context Protocol)客户端管理功能。
6
+
7
+ 主要功能:
8
+ - 管理 MCP 服务器连接
9
+ - 暴露 MCP 工具和资源
10
+ - 支持 STDIO 传输类型连接
11
+ - 提供工具调用和资源读取接口
12
+
13
+ 类说明:
14
+ - McpClientManager: MCP 客户端管理器类
15
+
16
+ 使用示例:
17
+ >>> from illusion.mcp.client import McpClientManager
18
+ >>> from illusion.mcp.types import McpStdioServerConfig
19
+ >>>
20
+ >>> configs = {"my_server": McpStdioServerConfig(command="node", args=["server.js"])}
21
+ >>> manager = McpClientManager(configs)
22
+ >>> await manager.connect_all()
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import asyncio
28
+ import sys
29
+ from contextlib import AsyncExitStack
30
+ from typing import Any
31
+
32
+ from mcp import ClientSession, StdioServerParameters
33
+ from mcp.client.stdio import stdio_client
34
+ from mcp.types import CallToolResult, ReadResourceResult
35
+
36
+ from illusion.mcp.types import (
37
+ McpConnectionStatus,
38
+ McpResourceInfo,
39
+ McpStdioServerConfig,
40
+ McpToolInfo,
41
+ )
42
+
43
+
44
+ class McpClientManager:
45
+ """
46
+ MCP 客户端管理器
47
+
48
+ 管理与 MCP 服务器的连接,并暴露服务器提供的工具和资源。
49
+ 支持 STDIO 传输类型的服务器连接。
50
+
51
+ Attributes:
52
+ _server_configs: 服务器名称到配置的映射
53
+ _statuses: 服务器名称到连接状态的映射
54
+ _sessions: 服务器名称到客户端会话的映射
55
+ _stacks: 服务器名称到异步退出栈的映射
56
+
57
+ 使用示例:
58
+ >>> manager = McpClientManager(configs)
59
+ >>> await manager.connect_all()
60
+ >>> tools = manager.list_tools()
61
+ """
62
+
63
+ def __init__(self, server_configs: dict[str, object]) -> None:
64
+ """
65
+ 初始化 MCP 客户端管理器
66
+
67
+ Args:
68
+ server_configs: 服务器名称到配置的映射字典
69
+ """
70
+ self._server_configs = server_configs
71
+ # 初始化所有服务器状态为 pending(待连接)
72
+ self._statuses: dict[str, McpConnectionStatus] = {
73
+ name: McpConnectionStatus(
74
+ name=name,
75
+ state="pending",
76
+ transport=getattr(config, "type", "unknown"),
77
+ )
78
+ for name, config in server_configs.items()
79
+ }
80
+ self._sessions: dict[str, ClientSession] = {} # 存储活跃的客户端会话
81
+ self._stacks: dict[str, AsyncExitStack] = {} # 存储异步上下文管理器栈
82
+
83
+ async def connect_all(self) -> None:
84
+ """
85
+ 连接所有已配置的 STDIO 类型 MCP 服务器
86
+
87
+ 并行连接所有 STDIO 类型的服务器以加速启动,
88
+ 其他类型的服务器标记为失败(当前版本仅支持 STDIO)。
89
+ """
90
+ # 收集需要并行连接的 STDIO 服务器
91
+ stdio_tasks: list[tuple[str, McpStdioServerConfig]] = []
92
+ for name, config in self._server_configs.items():
93
+ if isinstance(config, McpStdioServerConfig):
94
+ stdio_tasks.append((name, config))
95
+ else:
96
+ # 其他传输类型标记为失败
97
+ self._statuses[name] = McpConnectionStatus(
98
+ name=name,
99
+ state="failed",
100
+ transport=config.type,
101
+ auth_configured=bool(getattr(config, "headers", None)),
102
+ detail=f"Unsupported MCP transport in current build: {config.type}",
103
+ )
104
+
105
+ # 并行连接所有 STDIO 服务器
106
+ if stdio_tasks:
107
+ await asyncio.gather(
108
+ *(self._connect_stdio(name, config) for name, config in stdio_tasks),
109
+ )
110
+
111
+ async def reconnect_all(self) -> None:
112
+ """
113
+ 重新连接所有已配置的服务器
114
+
115
+ 先关闭所有现有连接,然后重置状态并重新建立连接。
116
+ """
117
+ await self.close()
118
+ # 重置所有服务器状态为 pending
119
+ self._statuses = {
120
+ name: McpConnectionStatus(name=name, state="pending", transport=getattr(config, "type", "unknown"))
121
+ for name, config in self._server_configs.items()
122
+ }
123
+ await self.connect_all()
124
+
125
+ def update_server_config(self, name: str, config: object) -> None:
126
+ """
127
+ 替换内存中的服务器配置
128
+
129
+ Args:
130
+ name: 服务器名称
131
+ config: 新的服务器配置对象
132
+ """
133
+ self._server_configs[name] = config
134
+
135
+ def get_server_config(self, name: str) -> object | None:
136
+ """
137
+ 获取指定的服务器配置
138
+
139
+ Args:
140
+ name: 服务器名称
141
+
142
+ Returns:
143
+ 服务器配置对象,如果不存在则返回 None
144
+ """
145
+ return self._server_configs.get(name)
146
+
147
+ async def close(self) -> None:
148
+ """
149
+ 关闭所有活跃的 MCP 会话
150
+
151
+ 释放所有资源,包括关闭流和清理会话。
152
+ """
153
+ # 关闭所有异步上下文栈
154
+ for stack in list(self._stacks.values()):
155
+ await stack.aclose()
156
+ self._stacks.clear()
157
+ self._sessions.clear()
158
+
159
+ def list_statuses(self) -> list[McpConnectionStatus]:
160
+ """
161
+ 获取所有已配置服务器的状态列表
162
+
163
+ Returns:
164
+ 按服务器名称排序的连接状态列表
165
+ """
166
+ return [self._statuses[name] for name in sorted(self._statuses)]
167
+
168
+ def list_tools(self) -> list[McpToolInfo]:
169
+ """
170
+ 获取所有已连接 MCP 服务器提供的工具列表
171
+
172
+ Returns:
173
+ 合并后的工具信息列表
174
+ """
175
+ tools: list[McpToolInfo] = []
176
+ for status in self.list_statuses():
177
+ tools.extend(status.tools)
178
+ return tools
179
+
180
+ def list_resources(self, *, server_name: str | None = None) -> list[McpResourceInfo]:
181
+ """
182
+ 获取所有已连接 MCP 服务器提供的资源列表
183
+
184
+ Returns:
185
+ 合并后的资源信息列表
186
+ """
187
+ if server_name is not None:
188
+ status = self._statuses.get(server_name)
189
+ if status is None:
190
+ return []
191
+ return list(status.resources)
192
+ resources: list[McpResourceInfo] = []
193
+ for status in self.list_statuses():
194
+ resources.extend(status.resources)
195
+ return resources
196
+
197
+ async def call_tool(self, server_name: str, tool_name: str, arguments: dict[str, Any]) -> str:
198
+ """
199
+ 调用指定的 MCP 工具
200
+
201
+ 在指定服务器上调用工具并返回字符串形式的结果。
202
+
203
+ Args:
204
+ server_name: 服务器名称
205
+ tool_name: 工具名称
206
+ arguments: 工具参数字典
207
+
208
+ Returns:
209
+ 工具执行结果的字符串形式
210
+ """
211
+ session = self._require_session(server_name)
212
+ result: CallToolResult = await session.call_tool(tool_name, arguments)
213
+ parts: list[str] = []
214
+ # 处理返回的内容,支持文本和其他类型
215
+ for item in result.content:
216
+ if getattr(item, "type", None) == "text":
217
+ parts.append(getattr(item, "text", ""))
218
+ else:
219
+ parts.append(item.model_dump_json())
220
+ # 如果有结构化内容但没有文本 parts,添加结构化内容
221
+ if result.structuredContent and not parts:
222
+ parts.append(str(result.structuredContent))
223
+ # 如果没有输出,返回默认消息
224
+ if not parts:
225
+ parts.append("(no output)")
226
+ return "\n".join(parts).strip()
227
+
228
+ async def read_resource(self, server_name: str, uri: str) -> str:
229
+ """
230
+ 读取指定的 MCP 资源
231
+
232
+ 从指定服务器读取资源并返回字符串形式的内容。
233
+
234
+ Args:
235
+ server_name: 服务器名称
236
+ uri: 资源统一标识符
237
+
238
+ Returns:
239
+ 资源内容的字符串形式
240
+ """
241
+ session = self._require_session(server_name)
242
+ result: ReadResourceResult = await session.read_resource(uri)
243
+ parts: list[str] = []
244
+ for item in result.contents:
245
+ text = getattr(item, "text", None)
246
+ if text is not None:
247
+ parts.append(text)
248
+ else:
249
+ parts.append(str(getattr(item, "blob", "")))
250
+ return "\n".join(parts).strip()
251
+
252
+ async def _connect_stdio(self, name: str, config: McpStdioServerConfig) -> None:
253
+ """
254
+ 连接 STDIO 类型的 MCP 服务器
255
+
256
+ 建立与 STDIO 服务器的连接,初始化会话,并获取服务器提供的工具和资源列表。
257
+
258
+ Args:
259
+ name: 服务器名称
260
+ config: STDIO 服务器配置
261
+ """
262
+ stack = AsyncExitStack()
263
+ try:
264
+ # 确定 stderr 输出目标:如果配置了 log_file 则重定向到文件
265
+ errlog = sys.stderr
266
+ if config.log_file:
267
+ from pathlib import Path
268
+ log_path = Path(config.log_file)
269
+ log_path.parent.mkdir(parents=True, exist_ok=True)
270
+ errlog = open(log_path, "a", encoding="utf-8")
271
+ stack.callback(errlog.close)
272
+
273
+ # 创建 STDIO 客户端连接
274
+ read_stream, write_stream = await stack.enter_async_context(
275
+ stdio_client(
276
+ StdioServerParameters(
277
+ command=config.command,
278
+ args=config.args,
279
+ env=config.env,
280
+ cwd=config.cwd,
281
+ ),
282
+ errlog=errlog,
283
+ )
284
+ )
285
+ # 创建客户端会话
286
+ session = await stack.enter_async_context(ClientSession(read_stream, write_stream))
287
+ await session.initialize()
288
+ # 获取服务器提供的工具列表
289
+ tool_result = await session.list_tools()
290
+ # 转换工具信息为内部数据模型
291
+ tools = [
292
+ McpToolInfo(
293
+ server_name=name,
294
+ name=tool.name,
295
+ description=tool.description or "",
296
+ input_schema=dict(tool.inputSchema or {"type": "object", "properties": {}}),
297
+ )
298
+ for tool in tool_result.tools
299
+ ]
300
+ # 获取服务器提供的资源列表(可选能力,服务器可能不支持)
301
+ resources: list[McpResourceInfo] = []
302
+ try:
303
+ resource_result = await session.list_resources()
304
+ resources = [
305
+ McpResourceInfo(
306
+ server_name=name,
307
+ name=resource.name or str(resource.uri),
308
+ uri=str(resource.uri),
309
+ description=resource.description or "",
310
+ )
311
+ for resource in resource_result.resources
312
+ ]
313
+ except Exception:
314
+ # 服务器不支持 resources 能力,忽略错误
315
+ pass
316
+ # 获取资源模板(部分服务器只暴露模板,不暴露静态资源)
317
+ try:
318
+ template_result = await session.list_resource_templates()
319
+ template_items = getattr(template_result, "resourceTemplates", None)
320
+ if template_items is None:
321
+ template_items = getattr(template_result, "resource_templates", [])
322
+ for template in template_items or []:
323
+ template_uri = str(
324
+ getattr(template, "uriTemplate", None)
325
+ or getattr(template, "uri_template", None)
326
+ or ""
327
+ ).strip()
328
+ if not template_uri:
329
+ continue
330
+ if any(item.uri == template_uri for item in resources):
331
+ continue
332
+ resources.append(
333
+ McpResourceInfo(
334
+ server_name=name,
335
+ name=getattr(template, "name", None) or template_uri,
336
+ uri=template_uri,
337
+ description=getattr(template, "description", "") or "",
338
+ )
339
+ )
340
+ except Exception:
341
+ # 服务器不支持 resource templates 能力,忽略错误
342
+ pass
343
+ # 保存会话和栈
344
+ self._sessions[name] = session
345
+ self._stacks[name] = stack
346
+ # 更新连接状态为已连接
347
+ self._statuses[name] = McpConnectionStatus(
348
+ name=name,
349
+ state="connected",
350
+ transport=config.type,
351
+ auth_configured=bool(config.env),
352
+ tools=tools,
353
+ resources=resources,
354
+ )
355
+ except Exception as exc:
356
+ # 连接失败,清理资源并更新状态
357
+ await stack.aclose()
358
+ self._statuses[name] = McpConnectionStatus(
359
+ name=name,
360
+ state="failed",
361
+ transport=config.type,
362
+ auth_configured=bool(config.env),
363
+ detail=str(exc),
364
+ )
365
+
366
+ def _require_session(self, server_name: str) -> ClientSession:
367
+ """获取服务器会话,不存在时抛出可读错误。"""
368
+ session = self._sessions.get(server_name)
369
+ if session is not None:
370
+ return session
371
+ status = self._statuses.get(server_name)
372
+ if status is None:
373
+ raise ValueError(f"Unknown MCP server: {server_name}")
374
+ detail = f" ({status.detail})" if status.detail else ""
375
+ raise ValueError(
376
+ f"MCP server '{server_name}' is not connected (state={status.state}{detail})"
377
+ )