ghoshell-moss 0.1.0a0__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 (125) hide show
  1. ghoshell_moss/README.md +7 -0
  2. ghoshell_moss/__init__.py +15 -0
  3. ghoshell_moss/channels/README.md +5 -0
  4. ghoshell_moss/channels/__init__.py +1 -0
  5. ghoshell_moss/channels/mac_channel.py +99 -0
  6. ghoshell_moss/cli/README.md +5 -0
  7. ghoshell_moss/cli/__init__.py +1 -0
  8. ghoshell_moss/compatible/README.md +7 -0
  9. ghoshell_moss/compatible/__init__.py +1 -0
  10. ghoshell_moss/compatible/mcp_channel/__init__.py +3 -0
  11. ghoshell_moss/compatible/mcp_channel/mcp_channel.py +468 -0
  12. ghoshell_moss/compatible/mcp_channel/types.py +17 -0
  13. ghoshell_moss/compatible/mcp_channel/utils.py +25 -0
  14. ghoshell_moss/core/README.md +6 -0
  15. ghoshell_moss/core/__init__.py +13 -0
  16. ghoshell_moss/core/concepts/README.md +20 -0
  17. ghoshell_moss/core/concepts/__init__.py +118 -0
  18. ghoshell_moss/core/concepts/channel.py +813 -0
  19. ghoshell_moss/core/concepts/command.py +981 -0
  20. ghoshell_moss/core/concepts/errors.py +73 -0
  21. ghoshell_moss/core/concepts/interpreter.py +365 -0
  22. ghoshell_moss/core/concepts/shell.py +332 -0
  23. ghoshell_moss/core/concepts/speech.py +548 -0
  24. ghoshell_moss/core/concepts/states.py +216 -0
  25. ghoshell_moss/core/concepts/topics.py +186 -0
  26. ghoshell_moss/core/ctml/README.md +9 -0
  27. ghoshell_moss/core/ctml/__init__.py +5 -0
  28. ghoshell_moss/core/ctml/elements.py +480 -0
  29. ghoshell_moss/core/ctml/interpreter.py +558 -0
  30. ghoshell_moss/core/ctml/prompt.py +9 -0
  31. ghoshell_moss/core/ctml/prompt_v1.md +174 -0
  32. ghoshell_moss/core/ctml/token_parser.py +427 -0
  33. ghoshell_moss/core/duplex/README.md +9 -0
  34. ghoshell_moss/core/duplex/__init__.py +54 -0
  35. ghoshell_moss/core/duplex/connection.py +57 -0
  36. ghoshell_moss/core/duplex/protocol.py +244 -0
  37. ghoshell_moss/core/duplex/provider.py +650 -0
  38. ghoshell_moss/core/duplex/proxy.py +1046 -0
  39. ghoshell_moss/core/duplex/thread_channel.py +180 -0
  40. ghoshell_moss/core/helpers/README.md +3 -0
  41. ghoshell_moss/core/helpers/__init__.py +1 -0
  42. ghoshell_moss/core/helpers/asyncio_utils.py +215 -0
  43. ghoshell_moss/core/helpers/func.py +166 -0
  44. ghoshell_moss/core/helpers/result.py +104 -0
  45. ghoshell_moss/core/helpers/stream.py +141 -0
  46. ghoshell_moss/core/helpers/token_filters.py +48 -0
  47. ghoshell_moss/core/py_channel.py +599 -0
  48. ghoshell_moss/core/shell/README.md +3 -0
  49. ghoshell_moss/core/shell/__init__.py +2 -0
  50. ghoshell_moss/core/shell/channel_runtime.py +632 -0
  51. ghoshell_moss/core/shell/main_channel.py +36 -0
  52. ghoshell_moss/core/shell/shell_impl.py +329 -0
  53. ghoshell_moss/core/shell/shell_runtime.py +431 -0
  54. ghoshell_moss/message/README.md +13 -0
  55. ghoshell_moss/message/__init__.py +4 -0
  56. ghoshell_moss/message/abcd.py +592 -0
  57. ghoshell_moss/message/adapters/__init__.py +1 -0
  58. ghoshell_moss/message/adapters/openai_adapter.py +101 -0
  59. ghoshell_moss/message/addtions.py +17 -0
  60. ghoshell_moss/message/contents.py +149 -0
  61. ghoshell_moss/message/deltas.py +32 -0
  62. ghoshell_moss/message/utils.py +15 -0
  63. ghoshell_moss/speech/README.md +9 -0
  64. ghoshell_moss/speech/__init__.py +23 -0
  65. ghoshell_moss/speech/mock.py +124 -0
  66. ghoshell_moss/speech/player/__init__.py +1 -0
  67. ghoshell_moss/speech/player/base_player.py +250 -0
  68. ghoshell_moss/speech/player/pulseaudio_player.py +74 -0
  69. ghoshell_moss/speech/player/pyaudio_player.py +71 -0
  70. ghoshell_moss/speech/stream_tts_speech.py +183 -0
  71. ghoshell_moss/speech/volcengine_tts/__init__.py +9 -0
  72. ghoshell_moss/speech/volcengine_tts/protocol.py +552 -0
  73. ghoshell_moss/speech/volcengine_tts/tts.py +728 -0
  74. ghoshell_moss/transports/README.md +19 -0
  75. ghoshell_moss/transports/__init__.py +1 -0
  76. ghoshell_moss/transports/redis_channel/__init__.py +7 -0
  77. ghoshell_moss/transports/redis_channel/redis_channel.py +228 -0
  78. ghoshell_moss/transports/ws_channel/__init__.py +7 -0
  79. ghoshell_moss/transports/ws_channel/ws_channel.py +209 -0
  80. ghoshell_moss/transports/zmq_channel/__init__.py +2 -0
  81. ghoshell_moss/transports/zmq_channel/zmq_channel.py +403 -0
  82. ghoshell_moss/transports/zmq_channel/zmq_hub.py +340 -0
  83. ghoshell_moss-0.1.0a0.dist-info/METADATA +159 -0
  84. ghoshell_moss-0.1.0a0.dist-info/RECORD +125 -0
  85. ghoshell_moss-0.1.0a0.dist-info/WHEEL +4 -0
  86. ghoshell_moss-0.1.0a0.dist-info/entry_points.txt +4 -0
  87. ghoshell_moss-0.1.0a0.dist-info/licenses/LICENSE +200 -0
  88. ghoshell_moss-0.1.0a0.dist-info/licenses/NOTICE +18 -0
  89. ghoshell_moss_contrib/README.md +4 -0
  90. ghoshell_moss_contrib/__init__.py +0 -0
  91. ghoshell_moss_contrib/agent/README.md +5 -0
  92. ghoshell_moss_contrib/agent/__init__.py +23 -0
  93. ghoshell_moss_contrib/agent/chat/__init__.py +1 -0
  94. ghoshell_moss_contrib/agent/chat/base.py +42 -0
  95. ghoshell_moss_contrib/agent/chat/console.py +202 -0
  96. ghoshell_moss_contrib/agent/chat/queue.py +191 -0
  97. ghoshell_moss_contrib/agent/depends.py +12 -0
  98. ghoshell_moss_contrib/agent/output.py +114 -0
  99. ghoshell_moss_contrib/agent/simple_agent.py +367 -0
  100. ghoshell_moss_contrib/channels/__init__.py +0 -0
  101. ghoshell_moss_contrib/channels/docs_reader.py +9 -0
  102. ghoshell_moss_contrib/channels/mermaid_draw.py +79 -0
  103. ghoshell_moss_contrib/channels/mpv_video.py +195 -0
  104. ghoshell_moss_contrib/channels/opencv_vision.py +307 -0
  105. ghoshell_moss_contrib/channels/project_manager.py +9 -0
  106. ghoshell_moss_contrib/channels/screen_capture.py +231 -0
  107. ghoshell_moss_contrib/channels/slide_studio.py +302 -0
  108. ghoshell_moss_contrib/channels/terminal.py +9 -0
  109. ghoshell_moss_contrib/channels/web_bookmark.py +75 -0
  110. ghoshell_moss_contrib/example_ws.py +148 -0
  111. ghoshell_moss_contrib/gui/__init__.py +0 -0
  112. ghoshell_moss_contrib/gui/image_viewer.py +86 -0
  113. ghoshell_moss_contrib/gui/slide_studio_creator.py +260 -0
  114. ghoshell_moss_contrib/prototypes/README.md +4 -0
  115. ghoshell_moss_contrib/prototypes/__init__.py +1 -0
  116. ghoshell_moss_contrib/prototypes/ros2_robot/README.md +11 -0
  117. ghoshell_moss_contrib/prototypes/ros2_robot/__init__.py +1 -0
  118. ghoshell_moss_contrib/prototypes/ros2_robot/abcd.py +578 -0
  119. ghoshell_moss_contrib/prototypes/ros2_robot/joint_parsers.py +26 -0
  120. ghoshell_moss_contrib/prototypes/ros2_robot/main_channel.py +237 -0
  121. ghoshell_moss_contrib/prototypes/ros2_robot/manager.py +84 -0
  122. ghoshell_moss_contrib/prototypes/ros2_robot/mocks.py +51 -0
  123. ghoshell_moss_contrib/prototypes/ros2_robot/models.py +407 -0
  124. ghoshell_moss_contrib/prototypes/ros2_robot/ros2_controller.py +203 -0
  125. ghoshell_moss_contrib/prototypes/ros2_robot/ros2_node.py +209 -0
@@ -0,0 +1,7 @@
1
+ # 目录介绍
2
+
3
+ - core: ghoshell moss 的核心功能模块
4
+ - message: 兼容性的模型消息协议. 暂时放到 ghoshell-moss 库, 未来可能迁出
5
+ - transports: 通过 provider -> proxy 范式, 跨进程的构建 channel 之间的双工通讯.
6
+ - compatible: 兼容性模块, 用来兼容行业生态. 比如 claude mcp 和 claude skills.
7
+ - channels: ghoshell-moss 库认为需要开箱自带的 channel 实现.
@@ -0,0 +1,15 @@
1
+ from ghoshell_container import (
2
+ Container,
3
+ IoCContainer,
4
+ get_container,
5
+ set_container,
6
+ )
7
+
8
+ from ghoshell_moss.core import *
9
+ from ghoshell_moss.message import *
10
+
11
+ """
12
+ Ghoshell MOSS 库的 facade, 用来存放最常用的类库引用.
13
+
14
+ 考虑只对外暴露最基础的常用函数.
15
+ """
@@ -0,0 +1,5 @@
1
+ # 关于 channels
2
+
3
+ 本目录实现 MOSS 协议认为必要的类型 Channel. 主要是业务 channel, 通常基于 PyChannel 实现.
4
+
5
+ Beta 版本会包含部分.
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,99 @@
1
+ import asyncio
2
+ from typing import Any, Optional
3
+
4
+ from ghoshell_moss.core import PyChannel
5
+
6
+ __all__ = ["new_mac_control_channel"]
7
+
8
+
9
+ class JXAError(Exception):
10
+ """JXA 执行错误"""
11
+
12
+ pass
13
+
14
+
15
+ async def run(*, timeout: Optional[float] = 30.0, text__: str = "") -> Any:
16
+ """
17
+ 在当前 MAC 上执行一个 JXA 函数, 子进程阻塞到执行完毕.
18
+ 如果你需要与 mac 应用做交互, 又没有别的手段时, 可以用这个办法.
19
+
20
+ :param timeout: 过期时间.
21
+ :param text__: jxa 脚本的源码.
22
+
23
+ CTML 使用举例:
24
+ <chan_name:run><![CDATA[
25
+ (function() {
26
+ 'use strict';
27
+
28
+ // 1. 使用 Bundle Identifier 获取日历应用对象
29
+ var Calendar = Application('com.apple.iCal');
30
+
31
+ // 2. 检查是否正在运行
32
+ var wasRunning = Calendar.running();
33
+
34
+ // 3. 尝试激活 (如果没有运行,这会启动它)
35
+ Calendar.activate();
36
+
37
+ // 4. 等待一小会儿,让系统处理激活请求
38
+ delay(0.5); // JXA 中的 delay 函数,单位秒
39
+
40
+ // 5. 再次检查运行状态和窗口状态
41
+ var isRunning = Calendar.running();
42
+ var windowCount = Calendar.windows ? Calendar.windows.length : 0;
43
+
44
+ // 6. 返回结构化的调试信息
45
+ return {
46
+ success: true,
47
+ action: 'activate_calendar',
48
+ bundleId: 'com.apple.iCal',
49
+ wasRunning: wasRunning,
50
+ isRunning: isRunning,
51
+ windowCount: windowCount,
52
+ timestamp: Date.now()
53
+ };
54
+ })();
55
+ ]]></chan_name:run>
56
+
57
+ :return: 返回操作结果. 你必须等操作结果到手后, 才能知道它运行的效果如何.
58
+ """
59
+ cmd = ["osascript", "-l", "JavaScript", "-"]
60
+
61
+ process = None
62
+ try:
63
+ process = await asyncio.create_subprocess_exec(
64
+ *cmd, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
65
+ )
66
+
67
+ stdout, stderr = await asyncio.wait_for(process.communicate(input=text__.encode("utf-8")), timeout=timeout)
68
+
69
+ if process.returncode != 0:
70
+ error_msg = stderr.decode("utf-8", errors="replace").strip()
71
+ raise JXAError(f"JXA 执行失败 (code: {process.returncode}): {error_msg}")
72
+
73
+ output = stdout.decode("utf-8", errors="replace").strip()
74
+
75
+ # 尝试解析 JSON
76
+ return output
77
+
78
+ except asyncio.TimeoutError:
79
+ if process and process.returncode is None:
80
+ process.kill()
81
+ await process.wait()
82
+ raise
83
+
84
+
85
+ def new_mac_control_channel(
86
+ name: str = "mac_control",
87
+ description: str = "使用 jxa 语法来操作当前所在 mac",
88
+ ) -> PyChannel:
89
+ """
90
+ 创建一个控制 mac 的 channel.
91
+ """
92
+ mac_jxa_channel = PyChannel(
93
+ name=name,
94
+ description=description,
95
+ block=True,
96
+ )
97
+
98
+ mac_jxa_channel.build.command()(run)
99
+ return mac_jxa_channel
@@ -0,0 +1,5 @@
1
+ # 关于 CLI
2
+
3
+ 本目录存放项目相关的常用 cli 工具.
4
+
5
+ alpha 版本一个都木有.
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,7 @@
1
+ # 关于 Compatible
2
+
3
+ 这里是 MOSS 架构兼容其它实现的相关类库放置.
4
+ 预计要兼容:
5
+
6
+ - anthropic mcp: 将 mcp 的工具调用转化为 command.
7
+ - anthropic skill: 将 skill 的整套机制, 转化为单一的 channel.
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,3 @@
1
+ from ghoshell_moss.compatible.mcp_channel.mcp_channel import MCPChannel
2
+
3
+ __all__ = ["MCPChannel"]
@@ -0,0 +1,468 @@
1
+ import json
2
+ import logging
3
+ from collections.abc import Callable, Coroutine
4
+ from typing import Any, Generic, Optional, TypeVar
5
+
6
+ from ghoshell_moss import CommandError, CommandErrorCode
7
+ from ghoshell_moss.compatible.mcp_channel.utils import mcp_call_tool_result_to_message
8
+ from ghoshell_moss.core.concepts.states import MemoryStateStore, StateStore
9
+
10
+ try:
11
+ import mcp
12
+ from mcp import types
13
+ except ImportError:
14
+ raise ImportError("Could not import mcp. Please install ghoshell-moss[mcp].")
15
+
16
+ import asyncio
17
+
18
+ from ghoshell_common.helpers import uuid
19
+ from ghoshell_container import Container, IoCContainer
20
+
21
+ from ghoshell_moss.core.concepts.channel import Builder, Channel, ChannelBroker, ChannelMeta
22
+ from ghoshell_moss.core.concepts.command import (
23
+ Command,
24
+ CommandDeltaType,
25
+ CommandMeta,
26
+ CommandTask,
27
+ CommandWrapper,
28
+ )
29
+
30
+ R = TypeVar("R") # 泛型结果类型
31
+
32
+
33
+ class MCPChannelBroker(ChannelBroker, Generic[R]):
34
+ """MCPChannel的运行时客户端,负责对接MCP服务"""
35
+
36
+ MCP_CONTAINER_TYPES: list[str] = ["array", "object"]
37
+
38
+ MCP_PY_TYPES_TRANS_TABLE: dict[str, str] = {
39
+ "string": "str",
40
+ "integer": "int",
41
+ "number": "float",
42
+ "boolean": "bool",
43
+ "array": "list",
44
+ "object": "dict",
45
+ }
46
+
47
+ COMMAND_DELTA_PARAMTER: str = f"{CommandDeltaType.TEXT.value}:str"
48
+
49
+ def __init__(
50
+ self,
51
+ *,
52
+ name: str,
53
+ mcp_client: mcp.ClientSession,
54
+ container: Optional[IoCContainer] = None,
55
+ ):
56
+ self._name = name
57
+ self._mcp_client: Optional[mcp.ClientSession] = mcp_client # MCP客户端实例
58
+ self._commands: dict[str, Command] = {} # 映射后的Mosshell Command
59
+ self._meta: Optional[ChannelMeta] = None # Channel元信息
60
+ self._running = False # 运行状态标记
61
+ self._logger: logging.Logger | None = None
62
+ self._id = uuid()
63
+ self._container = Container(parent=container, name="mcp_channel:" + self._name)
64
+ self._states: Optional[StateStore] = None
65
+
66
+ def children(self) -> dict[str, "Channel"]:
67
+ return {}
68
+
69
+ @property
70
+ def container(self) -> IoCContainer:
71
+ return self._container
72
+
73
+ @property
74
+ def id(self) -> str:
75
+ return self._id
76
+
77
+ def name(self) -> str:
78
+ return self._name
79
+
80
+ @property
81
+ def logger(self) -> logging.Logger:
82
+ if self._logger is None:
83
+ self._logger = self.container.get(logging.Logger) or logging.getLogger("moss")
84
+ return self._logger
85
+
86
+ # --- ChannelBroker 核心方法实现 --- #
87
+ async def start(self) -> None:
88
+ """启动MCP客户端并同步工具元信息"""
89
+ if self._running:
90
+ return
91
+
92
+ # 同步远端工具元信息
93
+ try:
94
+ await asyncio.to_thread(self._container.bootstrap)
95
+ initialize_result = await self._mcp_client.initialize() # 初始化MCP连接
96
+ tools = await self._mcp_client.list_tools()
97
+
98
+ # 转换为Mosshell Command和ChannelMeta
99
+ self._meta = self._build_channel_meta(initialize_result, tools)
100
+ self._running = True
101
+ except Exception as e:
102
+ raise RuntimeError(f"MCP tool discovery failed: {str(e)}") from e
103
+
104
+ @property
105
+ def states(self) -> StateStore:
106
+ if self._states is None:
107
+ _states = self._container.get(StateStore)
108
+ if _states is None:
109
+ _states = MemoryStateStore(self._name)
110
+ self._container.set(StateStore, _states)
111
+ self._states = _states
112
+ return self._states
113
+
114
+ async def close(self) -> None:
115
+ if not self._running:
116
+ return
117
+ await asyncio.to_thread(self._container.shutdown)
118
+
119
+ def is_running(self) -> bool:
120
+ return self._running
121
+
122
+ def meta(self) -> ChannelMeta:
123
+ # todo: 还没有实现动态更新, 主要是更新 command
124
+ if not self.is_running():
125
+ raise RuntimeError(f"Channel client {self._name} is not running")
126
+ return self._meta.model_copy()
127
+
128
+ async def refresh_meta(self) -> None:
129
+ # todo: shall refresh command metas
130
+ return None
131
+
132
+ def is_connected(self) -> bool:
133
+ # todo: 检查状态.
134
+ return self.is_running()
135
+
136
+ async def wait_connected(self) -> None:
137
+ # todo: 检查状态.
138
+ return
139
+
140
+ def commands(self, available_only: bool = True) -> dict[str, Command]:
141
+ # todo: 这里每次更新, 和上面好像冲突.
142
+ meta = self.meta()
143
+ result = {}
144
+ for command_meta in meta.commands:
145
+ if not available_only or command_meta.available:
146
+ func = self._get_command_func(command_meta)
147
+ command = CommandWrapper(meta=command_meta, func=func)
148
+ result[command_meta.name] = command
149
+ return result
150
+
151
+ def get_command(self, name: str) -> Optional[Command]:
152
+ meta = self.meta()
153
+ for command_meta in meta.commands:
154
+ if command_meta.name == name:
155
+ func = self._get_command_func(command_meta)
156
+ return CommandWrapper(meta=command_meta, func=func)
157
+ return None
158
+
159
+ def _get_command_func(self, meta: CommandMeta) -> Callable[[...], Coroutine[None, None, Any]] | None:
160
+ name = meta.name
161
+
162
+ args_schema_properties = meta.args_schema.get("properties", {})
163
+ required_args_list = meta.args_schema.get("required", [])
164
+ schema_param_count = len(args_schema_properties)
165
+ required_schema_param_count = len(required_args_list)
166
+
167
+ # 回调服务端.
168
+ async def _server_caller_as_command(*args, **kwargs):
169
+ # 调用MCP客户端执行工具
170
+ try:
171
+ if required_schema_param_count > schema_param_count:
172
+ raise CommandError(
173
+ code=CommandErrorCode.INVALID_PARAMETER.value,
174
+ message=(
175
+ "MCP tool: invalid parameter count, required parameter: "
176
+ f"{required_schema_param_count}, schema parameter: {schema_param_count}"
177
+ ),
178
+ )
179
+
180
+ param_count = len(args) + len(kwargs)
181
+ final_kwargs = {}
182
+ if schema_param_count == 0: # do nothing
183
+ if not param_count == 0:
184
+ raise CommandError(
185
+ code=CommandErrorCode.INVALID_PARAMETER.value,
186
+ message=f"MCP tool: no parameter, invalid, args={args}, kwargs={kwargs}",
187
+ )
188
+ else: # schema_param_count > 1
189
+ if not (param_count == 1 or required_schema_param_count <= param_count <= schema_param_count):
190
+ raise CommandError(
191
+ code=CommandErrorCode.INVALID_PARAMETER.value,
192
+ message=f"MCP tool: invalid parameters, invalid, args={args}, kwargs={kwargs}",
193
+ )
194
+ if param_count == 1:
195
+ if len(args) == 1:
196
+ if required_schema_param_count == 1:
197
+ if type(args[0]) is not str:
198
+ [param_name, param_info], *_ = args_schema_properties.items()
199
+ if param_type := param_info.get("type", None):
200
+ if type(args[0]).__name__ == self._mcp_type_2_py_type(param_type):
201
+ final_kwargs[param_name] = args[0]
202
+
203
+ if not len(final_kwargs):
204
+ try:
205
+ final_kwargs = json.loads(args[0])
206
+ except TypeError as e:
207
+ raise CommandError(
208
+ code=CommandErrorCode.VALUE_ERROR.value,
209
+ message=f'MCP tool: invalid "text__" type, {str(e)}',
210
+ )
211
+ except json.JSONDecodeError as e:
212
+ raise CommandError(
213
+ code=CommandErrorCode.VALUE_ERROR.value,
214
+ message=(
215
+ f"MCP tool: invalid `text__` parameter format, INVALID JSON schema, {e}"
216
+ ),
217
+ )
218
+ else:
219
+ if "text__" in kwargs:
220
+ final_kwargs = json.loads(kwargs["text__"])
221
+ elif required_schema_param_count == 1:
222
+ param_name = required_args_list[0]
223
+ if param_name not in kwargs:
224
+ raise CommandError(
225
+ code=CommandErrorCode.INVALID_PARAMETER.value,
226
+ message=f'MCP tool: unknown parameter "{param_name}" parameter format.',
227
+ )
228
+ final_kwargs.update(kwargs)
229
+ else:
230
+ raise CommandError(
231
+ code=CommandErrorCode.INVALID_PARAMETER.value,
232
+ message=f'MCP tool: missing "text__" parameters, kwargs={kwargs}',
233
+ )
234
+ else:
235
+ for arg_name, arg in zip(args_schema_properties.keys(), args):
236
+ final_kwargs[arg_name] = arg
237
+ final_kwargs.update(kwargs)
238
+
239
+ mcp_result = await self._mcp_client.call_tool(
240
+ name=meta.name,
241
+ arguments=final_kwargs,
242
+ )
243
+ # convert to moss Message
244
+ return mcp_call_tool_result_to_message(mcp_result, name=self.name())
245
+ except mcp.McpError as e:
246
+ raise CommandError(code=CommandErrorCode.FAILED.value, message=f"MCP call failed: {str(e)}") from e
247
+ except Exception as e:
248
+ raise CommandError(
249
+ code=CommandErrorCode.FAILED.value, message=f"MCP tool execution failed: {str(e)}"
250
+ ) from e
251
+
252
+ return _server_caller_as_command
253
+
254
+ async def execute(self, task: CommandTask[R]) -> R:
255
+ if not self.is_running():
256
+ raise RuntimeError("MCPChannel is not running")
257
+ func = self._get_command_func(task.meta)
258
+ if func is None:
259
+ raise LookupError(f"Channel {self._name} can find command {task.meta.name}")
260
+ return await func(*task.args, **task.kwargs)
261
+
262
+ # --- 工具转Command的核心逻辑 --- #
263
+
264
+ def _convert_tools_to_command_metas(self, tools: list[types.Tool]) -> list[CommandMeta]:
265
+ """将MCP工具转换为Mosshell的CommandMeta"""
266
+ metas = []
267
+ for tool in tools:
268
+ tool_name = tool.name
269
+
270
+ # 生成符合Code as Prompt的interface(模型可见的函数签名)
271
+ interface, description = self._generate_code_as_prompt(tool)
272
+
273
+ metas.append(
274
+ CommandMeta(
275
+ name=tool_name,
276
+ description=description or "",
277
+ chan=self._name,
278
+ interface=interface,
279
+ available=True,
280
+ args_schema=tool.inputSchema,
281
+ delta_arg=CommandDeltaType.TEXT,
282
+ )
283
+ )
284
+ return metas
285
+
286
+ @staticmethod
287
+ def _mcp_type_2_py_type(param_info_type: str) -> str:
288
+ param_type = MCPChannelBroker.MCP_PY_TYPES_TRANS_TABLE.get(param_info_type.lower(), "Any")
289
+ return param_type
290
+
291
+ def _parse_schema(self, schema: dict) -> tuple[list, list]:
292
+ required_params = []
293
+ optional_params = []
294
+ required_param_docs = []
295
+ optional_param_docs = []
296
+
297
+ for param_name, param_info in schema.get("properties", {}).items():
298
+ # 确定参数类型
299
+ param_type = self._mcp_type_2_py_type(param_info.get("type", ""))
300
+
301
+ # 确定默认值
302
+ param_str = f"{param_name}: {param_type}"
303
+ if param_name not in schema.get("required", []):
304
+ default_value = "None" if param_type != "bool" else "False"
305
+ param_str += f"={default_value}"
306
+
307
+ # 根据是否必需参数,添加到不同的列表
308
+ if param_name in schema.get("required", []):
309
+ required_params.append(param_str)
310
+ # 添加参数文档
311
+ if "description" in param_info:
312
+ required_param_docs.append(f" :param {param_name}: {param_info['description']}")
313
+ else:
314
+ optional_params.append(param_str)
315
+ # 添加参数文档
316
+ if "description" in param_info:
317
+ optional_param_docs.append(f" :param {param_name}: {param_info['description']}")
318
+
319
+ return required_params + optional_params, required_param_docs + optional_param_docs
320
+
321
+ def _parse_schema_container(self, schema: dict) -> tuple[list, list]:
322
+ params = [self.COMMAND_DELTA_PARAMTER]
323
+ try:
324
+ required_param_docs = [
325
+ "param text__: 用 JSON 描述参数,它的 JSON Schema 如右:",
326
+ json.dumps(schema),
327
+ ]
328
+ except Exception as e:
329
+ raise e
330
+
331
+ return params, required_param_docs
332
+
333
+ def _parse_input_schema(self, input_schema: dict[str, Any], error_prefix="") -> tuple[list[str], list[str]]:
334
+ """解析inputSchema并提取参数信息和参数文档"""
335
+ # todo: 考虑直接将 json schema 作为 text__ 参数.
336
+ if not input_schema:
337
+ return [], []
338
+
339
+ params = []
340
+ param_docs = []
341
+ try:
342
+ # 解析inputSchema
343
+ schema = input_schema
344
+ if isinstance(schema, str):
345
+ schema = json.loads(schema)
346
+
347
+ if "properties" not in schema:
348
+ return params, param_docs
349
+
350
+ # 合并列表,必需参数在前,可选参数在后
351
+ # mcp_types = [i.get('type', '') for i in list(schema['properties'].values())]
352
+ # if any([x in self.MCP_CONTAINER_TYPES for x in mcp_types]):
353
+ # params, param_docs = self._parse_schema_container(schema)
354
+ # else:
355
+ # params, param_docs = self._parse_schema(schema)
356
+ params, param_docs = self._parse_schema_container(schema)
357
+
358
+ except Exception as e:
359
+ print(f"{error_prefix}解析inputSchema出错: {e}")
360
+ return params, param_docs
361
+
362
+ def _adjust_description(self, description: str, param_doc: str) -> str:
363
+ return f"{description}\n{param_doc}\n"
364
+
365
+ def _generate_code_as_prompt(self, tool: types.Tool) -> tuple[str, str]:
366
+ """生成模型可见的Command接口(Code as Prompt)"""
367
+
368
+ # 提取函数名(将连字符替换为下划线)
369
+ function_name = tool.name.replace("-", "_")
370
+
371
+ # 提取参数信息
372
+ params, param_docs = self._parse_input_schema(tool.inputSchema, "")
373
+
374
+ description = tool.description or ""
375
+ if len(params) == 1 and params[0] == self.COMMAND_DELTA_PARAMTER:
376
+ description = self._adjust_description(description, "".join(param_docs))
377
+
378
+ # 生成Async函数签名(符合Python语法)
379
+ interface = (
380
+ f"async def {function_name}({', '.join(params)}) -> Any:\n"
381
+ f" '''\n"
382
+ f" {description}\n"
383
+ # f" {''.join(param_docs)}\n"
384
+ f" '''\n"
385
+ f" pass"
386
+ )
387
+ return interface, description
388
+
389
+ def _build_channel_meta(
390
+ self, initialize_result: types.InitializeResult, tool_result: types.ListToolsResult
391
+ ) -> ChannelMeta:
392
+ """构建Channel元信息(包含所有工具的CommandMeta)"""
393
+ return ChannelMeta(
394
+ name=self._name,
395
+ channel_id=self._name,
396
+ available=True,
397
+ description=initialize_result.instructions or "",
398
+ commands=self._convert_tools_to_command_metas(tools=tool_result.tools),
399
+ children=[],
400
+ )
401
+
402
+ # --- 未使用的生命周期方法(默认空实现) --- #
403
+ async def policy_run(self) -> None:
404
+ pass
405
+
406
+ async def policy_pause(self) -> None:
407
+ pass
408
+
409
+ async def clear(self) -> None:
410
+ pass
411
+
412
+ def is_available(self) -> bool:
413
+ return True
414
+
415
+
416
+ class MCPChannel(Channel):
417
+ """对接MCP服务的Channel"""
418
+
419
+ def __init__(
420
+ self,
421
+ *,
422
+ name: str,
423
+ description: str,
424
+ mcp_client: mcp.ClientSession,
425
+ ):
426
+ self._name = name
427
+ self._desc = description
428
+ self._mcp_client = mcp_client
429
+ self._client: Optional[MCPChannelBroker] = None
430
+
431
+ # --- Channel 核心方法实现 --- #
432
+ def name(self) -> str:
433
+ return self._name
434
+
435
+ @property
436
+ def broker(self) -> ChannelBroker:
437
+ if not self._client or not self._client.is_running():
438
+ raise RuntimeError("MCPChannel not bootstrapped")
439
+ return self._client
440
+
441
+ @property
442
+ def build(self) -> Builder:
443
+ raise NotImplementedError("MCPChannel does not implement `build`")
444
+
445
+ def bootstrap(self, container: Optional[IoCContainer] = None) -> ChannelBroker:
446
+ if self._client is not None and self._client.is_running():
447
+ raise RuntimeError(f"Channel {self} has already been started.")
448
+
449
+ self._client = MCPChannelBroker(
450
+ name=self._name,
451
+ container=container,
452
+ mcp_client=self._mcp_client,
453
+ )
454
+
455
+ return self._client
456
+
457
+ # --- 未使用的Channel方法(默认空实现) --- #
458
+ def import_channels(self, *children: Channel) -> Channel:
459
+ raise NotImplementedError("MCPChannel does not support children")
460
+
461
+ def new_child(self, name: str) -> Channel:
462
+ raise NotImplementedError("MCPChannel does not support children")
463
+
464
+ def children(self) -> dict[str, Channel]:
465
+ return {}
466
+
467
+ def is_running(self) -> bool:
468
+ return self._client is not None and self._client.is_running()
@@ -0,0 +1,17 @@
1
+ from mcp import types as mcp_types
2
+
3
+ from ghoshell_moss.message import Addition
4
+
5
+
6
+ class MCPCallToolResultAddition(Addition, mcp_types.CallToolResult):
7
+ @classmethod
8
+ def keyword(cls) -> str:
9
+ return "mcp_call_tool_result"
10
+
11
+ @classmethod
12
+ def from_mcp_types(cls, mcp_result: mcp_types.CallToolResult):
13
+ return cls(
14
+ isError=mcp_result.isError,
15
+ structuredContent=mcp_result.structuredContent,
16
+ content=mcp_result.content,
17
+ )
@@ -0,0 +1,25 @@
1
+ from mcp import types
2
+
3
+ from ghoshell_moss import CommandError, CommandErrorCode
4
+ from ghoshell_moss.compatible.mcp_channel.types import MCPCallToolResultAddition
5
+ from ghoshell_moss.message import Base64Image, Message, Text
6
+
7
+
8
+ def mcp_call_tool_result_to_message(mcp_result: types.CallToolResult, name: str | None = None) -> Message:
9
+ if mcp_result.isError:
10
+ raise CommandError(
11
+ code=CommandErrorCode.FAILED.value,
12
+ message=f"MCP tool: call failed, {mcp_result.content}",
13
+ )
14
+ result = Message.new(role="assistant", name=name)
15
+ for mcp_content in mcp_result.content:
16
+ if isinstance(mcp_content, types.TextContent):
17
+ result.with_content(Text(text=mcp_content.text))
18
+ if isinstance(mcp_content, types.ImageContent):
19
+ result.with_content(Base64Image(image_type=mcp_content.mimeType, encoded=mcp_content.data))
20
+ if isinstance(mcp_content, types.AudioContent):
21
+ pass
22
+ pass
23
+
24
+ result.with_additions(MCPCallToolResultAddition.from_mcp_types(mcp_result))
25
+ return result
@@ -0,0 +1,6 @@
1
+ # core
2
+
3
+ 项目的核心目录, 包含:
4
+
5
+ - concepts/ # 基于代码即文档的思想, 将 moss 的抽象设计列入
6
+ -