langchain-agentx-python 2.3.4__py3-none-any.whl → 2.3.5__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 (89) hide show
  1. langchain_agentx/__init__.py +75 -64
  2. langchain_agentx/command/mcp/__init__.py +5 -0
  3. langchain_agentx/command/mcp/adapter.py +60 -0
  4. langchain_agentx/command/prompt_shell.py +123 -123
  5. langchain_agentx/command/registry.py +16 -0
  6. langchain_agentx/loop/config/loop_config.py +9 -0
  7. langchain_agentx/loop/graph/factory.py +70 -0
  8. langchain_agentx/loop/prompt/mcp_instructions.py +40 -0
  9. langchain_agentx/loop/subagent/context.py +24 -1
  10. langchain_agentx/loop/subagent/graph.py +6 -1
  11. langchain_agentx/loop/subagent/mcp_child_servers.py +117 -0
  12. langchain_agentx/loop/subagent/orchestrator.py +104 -0
  13. langchain_agentx/loop/subagent/runner.py +7 -0
  14. langchain_agentx/mcp/__init__.py +46 -0
  15. langchain_agentx/mcp/auth/__init__.py +5 -0
  16. langchain_agentx/mcp/auth/oauth_session.py +57 -0
  17. langchain_agentx/mcp/call.py +140 -0
  18. langchain_agentx/mcp/client_manager.py +584 -0
  19. langchain_agentx/mcp/config/__init__.py +12 -0
  20. langchain_agentx/mcp/config/env_expand.py +49 -0
  21. langchain_agentx/mcp/config/merge.py +66 -0
  22. langchain_agentx/mcp/config/policy.py +49 -0
  23. langchain_agentx/mcp/config/project_file.py +43 -0
  24. langchain_agentx/mcp/config/resolver.py +194 -0
  25. langchain_agentx/mcp/controller.py +123 -0
  26. langchain_agentx/mcp/elicitation.py +201 -0
  27. langchain_agentx/mcp/errors.py +32 -0
  28. langchain_agentx/mcp/events.py +42 -0
  29. langchain_agentx/mcp/fixtures/__init__.py +0 -0
  30. langchain_agentx/mcp/fixtures/echo_server.py +25 -0
  31. langchain_agentx/mcp/fixtures/elicit_server.py +37 -0
  32. langchain_agentx/mcp/fixtures/surface_server.py +61 -0
  33. langchain_agentx/mcp/loop_driver.py +111 -0
  34. langchain_agentx/mcp/result.py +84 -0
  35. langchain_agentx/mcp/stdio_connector.py +64 -0
  36. langchain_agentx/mcp/transports/__init__.py +9 -0
  37. langchain_agentx/mcp/transports/connector.py +64 -0
  38. langchain_agentx/mcp/transports/http.py +31 -0
  39. langchain_agentx/mcp/transports/sse.py +22 -0
  40. langchain_agentx/observability/events/__init__.py +3 -0
  41. langchain_agentx/observability/events/agent_event_projector.py +3 -0
  42. langchain_agentx/observability/events/mcp_event_sink.py +53 -0
  43. langchain_agentx/protocol/__init__.py +11 -0
  44. langchain_agentx/protocol/mcp/__init__.py +58 -0
  45. langchain_agentx/protocol/mcp/constants.py +22 -0
  46. langchain_agentx/protocol/mcp/naming.py +64 -0
  47. langchain_agentx/protocol/mcp/sanitization.py +71 -0
  48. langchain_agentx/protocol/mcp/truncate.py +22 -0
  49. langchain_agentx/protocol/mcp/types.py +151 -0
  50. langchain_agentx/session/agent_session.py +13 -3
  51. langchain_agentx/session/conversation_factory.py +9 -0
  52. langchain_agentx/session/conversation_session.py +11 -1
  53. langchain_agentx/session/factory.py +9 -0
  54. langchain_agentx/tool_runtime/inner_permission_chain.py +23 -3
  55. langchain_agentx/tool_runtime/loader.py +4 -0
  56. langchain_agentx/tool_runtime/loop_loader.py +158 -0
  57. langchain_agentx/tool_runtime/permission_rule_matcher.py +49 -0
  58. langchain_agentx/tool_runtime/permission_rules.py +10 -6
  59. langchain_agentx/tool_runtime/policy_decorator.py +70 -70
  60. langchain_agentx/tool_runtime/process_loader.py +3 -0
  61. langchain_agentx/tool_runtime/registry.py +4 -0
  62. langchain_agentx/tools/agent/loader.py +61 -2
  63. langchain_agentx/tools/agent/prompt_scope.py +1 -1
  64. langchain_agentx/tools/agent/registry/config.py +5 -1
  65. langchain_agentx/tools/bash/command_allow_rules.py +8 -2
  66. langchain_agentx/tools/mcp/__init__.py +15 -0
  67. langchain_agentx/tools/mcp/backend.py +50 -0
  68. langchain_agentx/tools/mcp/loader.py +234 -0
  69. langchain_agentx/tools/mcp/pool_binder.py +81 -0
  70. langchain_agentx/tools/mcp/prompt.py +27 -0
  71. langchain_agentx/tools/mcp/tool.py +228 -0
  72. langchain_agentx/tools/mcp_resource_list/__init__.py +5 -0
  73. langchain_agentx/tools/mcp_resource_list/models.py +12 -0
  74. langchain_agentx/tools/mcp_resource_list/prompt.py +24 -0
  75. langchain_agentx/tools/mcp_resource_list/tool.py +144 -0
  76. langchain_agentx/tools/mcp_resource_read/__init__.py +5 -0
  77. langchain_agentx/tools/mcp_resource_read/models.py +10 -0
  78. langchain_agentx/tools/mcp_resource_read/prompt.py +22 -0
  79. langchain_agentx/tools/mcp_resource_read/tool.py +124 -0
  80. langchain_agentx/tools/skill/catalog.py +26 -4
  81. langchain_agentx/tools/skill/mcp_ingest.py +118 -0
  82. langchain_agentx/workspace/config.py +10 -0
  83. langchain_agentx/workspace/path_canonical.py +24 -24
  84. langchain_agentx/workspace/project_memory_layout.py +18 -4
  85. {langchain_agentx_python-2.3.4.dist-info → langchain_agentx_python-2.3.5.dist-info}/METADATA +583 -582
  86. {langchain_agentx_python-2.3.4.dist-info → langchain_agentx_python-2.3.5.dist-info}/RECORD +89 -36
  87. {langchain_agentx_python-2.3.4.dist-info → langchain_agentx_python-2.3.5.dist-info}/LICENSE +0 -0
  88. {langchain_agentx_python-2.3.4.dist-info → langchain_agentx_python-2.3.5.dist-info}/WHEEL +0 -0
  89. {langchain_agentx_python-2.3.4.dist-info → langchain_agentx_python-2.3.5.dist-info}/top_level.txt +0 -0
@@ -1,64 +1,75 @@
1
- """
2
- LangChain-based agent utilities for CodeBaseX.
3
-
4
- This package provides:
5
- - LangChain-AgentX LangGraph agent factory (`loop`)
6
-
7
- Typical usage:
8
-
9
- ```python
10
- from langchain_agentx import create_loop_agent
11
- ```
12
- """
13
-
14
- __version__ = "2.3.4"
15
-
16
- from .loop import ( # noqa: F401
17
- create_loop_agent,
18
- )
19
-
20
- from .loop.hook import ( # noqa: F401
21
- ATTACHMENT_HOOK_BLOCKING_ERROR,
22
- ATTACHMENT_HOOK_CANCELLED,
23
- ATTACHMENT_HOOK_NON_BLOCKING_ERROR,
24
- ATTACHMENT_HOOK_STOPPED_CONTINUATION,
25
- ATTACHMENT_HOOK_SUCCESS,
26
- )
27
- from .observability import ( # noqa: F401
28
- EventAdapterState,
29
- LangchainAgentEvent,
30
- LangchainAgentEventType,
31
- LangGraphToLangchainAgentEventAdapter,
32
- TraceCollector,
33
- TraceSession,
34
- TraceSpan,
35
- )
36
- from .provider import CompatibleChatOpenAI # noqa: F401
37
- from .session import AgentSession, create_agent_session # noqa: F401
38
- from .health import HealthCheck, HealthReport, PreflightResult, preflight, sdk_health # noqa: F401
39
- from .workflow import BaseWorkflow # noqa: F401
40
-
41
- __all__ = [
42
- "ATTACHMENT_HOOK_BLOCKING_ERROR",
43
- "ATTACHMENT_HOOK_CANCELLED",
44
- "ATTACHMENT_HOOK_NON_BLOCKING_ERROR",
45
- "ATTACHMENT_HOOK_STOPPED_CONTINUATION",
46
- "ATTACHMENT_HOOK_SUCCESS",
47
- "create_loop_agent",
48
- "create_agent_session",
49
- "AgentSession",
50
- "BaseWorkflow",
51
- "EventAdapterState",
52
- "LangchainAgentEvent",
53
- "LangchainAgentEventType",
54
- "LangGraphToLangchainAgentEventAdapter",
55
- "TraceCollector",
56
- "TraceSession",
57
- "TraceSpan",
58
- "CompatibleChatOpenAI",
59
- "HealthCheck",
60
- "HealthReport",
61
- "PreflightResult",
62
- "preflight",
63
- "sdk_health",
64
- ]
1
+ """
2
+ LangChain-based agent utilities for CodeBaseX.
3
+
4
+ This package provides:
5
+ - LangChain-AgentX LangGraph agent factory (`loop`)
6
+
7
+ Typical usage:
8
+
9
+ ```python
10
+ from langchain_agentx import create_loop_agent
11
+ ```
12
+ """
13
+
14
+ __version__ = "2.3.5"
15
+
16
+ from .mcp.call import McpToolCaller
17
+ from .mcp.client_manager import McpClientManager, McpConnectedClient
18
+ from .mcp.controller import McpRuntimeController
19
+ from .mcp.config.resolver import McpConfigResolver
20
+ from .mcp.events import CollectingEventSink
21
+ from .loop import ( # noqa: F401
22
+ create_loop_agent,
23
+ )
24
+
25
+ from .loop.hook import ( # noqa: F401
26
+ ATTACHMENT_HOOK_BLOCKING_ERROR,
27
+ ATTACHMENT_HOOK_CANCELLED,
28
+ ATTACHMENT_HOOK_NON_BLOCKING_ERROR,
29
+ ATTACHMENT_HOOK_STOPPED_CONTINUATION,
30
+ ATTACHMENT_HOOK_SUCCESS,
31
+ )
32
+ from .observability import ( # noqa: F401
33
+ EventAdapterState,
34
+ LangchainAgentEvent,
35
+ LangchainAgentEventType,
36
+ LangGraphToLangchainAgentEventAdapter,
37
+ TraceCollector,
38
+ TraceSession,
39
+ TraceSpan,
40
+ )
41
+ from .provider import CompatibleChatOpenAI # noqa: F401
42
+ from .session import AgentSession, create_agent_session # noqa: F401
43
+ from .health import HealthCheck, HealthReport, PreflightResult, preflight, sdk_health # noqa: F401
44
+ from .workflow import BaseWorkflow # noqa: F401
45
+
46
+ __all__ = [
47
+ "ATTACHMENT_HOOK_BLOCKING_ERROR",
48
+ "ATTACHMENT_HOOK_CANCELLED",
49
+ "ATTACHMENT_HOOK_NON_BLOCKING_ERROR",
50
+ "ATTACHMENT_HOOK_STOPPED_CONTINUATION",
51
+ "ATTACHMENT_HOOK_SUCCESS",
52
+ "create_loop_agent",
53
+ "create_agent_session",
54
+ "AgentSession",
55
+ "CollectingEventSink",
56
+ "McpClientManager",
57
+ "McpConfigResolver",
58
+ "McpConnectedClient",
59
+ "McpRuntimeController",
60
+ "McpToolCaller",
61
+ "BaseWorkflow",
62
+ "EventAdapterState",
63
+ "LangchainAgentEvent",
64
+ "LangchainAgentEventType",
65
+ "LangGraphToLangchainAgentEventAdapter",
66
+ "TraceCollector",
67
+ "TraceSession",
68
+ "TraceSpan",
69
+ "CompatibleChatOpenAI",
70
+ "HealthCheck",
71
+ "HealthReport",
72
+ "PreflightResult",
73
+ "preflight",
74
+ "sdk_health",
75
+ ]
@@ -0,0 +1,5 @@
1
+ """command/mcp — MCP prompts → CommandDefinition 适配。"""
2
+
3
+ from langchain_agentx.command.mcp.adapter import McpPromptCommandAdapter
4
+
5
+ __all__ = ["McpPromptCommandAdapter"]
@@ -0,0 +1,60 @@
1
+ """MCP prompts/list → CommandDefinition。
2
+
3
+ 职责:
4
+ 对标 CC ``fetchCommandsForClient``:已连接 session 上 prompts/list,
5
+ 映射 ``command_type=prompt``、``source=mcp``、名 ``mcp__{server}__{prompt}``。
6
+
7
+ 链路位置:
8
+ McpClientManager.list_prompts(connect=False) → 本适配器 → CommandRegistry.register_many。
9
+
10
+ 当前裁剪:
11
+ 不 connect / 不 ensure;slash UX 由宿主投影;不扩展 CommandType。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import TYPE_CHECKING
17
+
18
+ from langchain_agentx.command.result import CommandResult
19
+ from langchain_agentx.command.types import CommandDefinition
20
+ from langchain_agentx.protocol.mcp.naming import build_mcp_tool_name
21
+ from langchain_agentx.protocol.mcp.types import McpPromptRef
22
+
23
+ if TYPE_CHECKING:
24
+ from langchain_agentx.command.context import CommandContext
25
+ from langchain_agentx.mcp.client_manager import McpClientManager
26
+
27
+
28
+ class McpPromptCommandAdapter:
29
+ """已连接 Manager 上的 prompts 适配;禁止在此拉起 session。"""
30
+
31
+ def __init__(self, manager: McpClientManager) -> None:
32
+ self._manager = manager
33
+
34
+ async def list_definitions(self) -> list[CommandDefinition]:
35
+ definitions: list[CommandDefinition] = []
36
+ for spec in self._manager.connected_specs():
37
+ refs = await self._manager.list_prompts(spec.name, connect=False)
38
+ for ref in refs:
39
+ definitions.append(self._to_definition(ref))
40
+ return definitions
41
+
42
+ def _to_definition(self, ref: McpPromptRef) -> CommandDefinition:
43
+ return CommandDefinition(
44
+ name=build_mcp_tool_name(ref.server_name, ref.name),
45
+ description=ref.description,
46
+ command_type="prompt",
47
+ source="mcp",
48
+ handler=self._make_handler(ref),
49
+ )
50
+
51
+ def _make_handler(self, ref: McpPromptRef):
52
+ manager = self._manager
53
+
54
+ async def _handler(context: CommandContext) -> CommandResult:
55
+ _ = context
56
+ messages = await manager.get_prompt(ref.server_name, ref.name, connect=False)
57
+ text = "\n\n".join(messages.messages)
58
+ return CommandResult(should_query=True, injected_prompt=text)
59
+
60
+ return _handler
@@ -1,123 +1,123 @@
1
- """
2
- command/prompt_shell.py — Prompt 内嵌 shell 命令执行(CC executeShellCommandsInPrompt 对齐)。
3
-
4
- 职责:
5
- 解析并执行 prompt 中的 !`command` 占位,将输出内联替换;执行前走路由权限检查。
6
-
7
- 链路位置:
8
- 由 command/builtin/commit* 在展开 prompt 前调用。
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import re
14
- from collections.abc import Callable, Sequence
15
- from pathlib import Path
16
- from typing import Protocol
17
-
18
- from langchain_agentx.tool_runtime.models import ToolExecutionContext
19
- from langchain_agentx.tool_runtime.permission_context import set_command_bash_allow_rules
20
- from langchain_agentx.tool_runtime.session_store import AgentSessionStore
21
- from langchain_agentx.utils.subprocess_text import TextSubprocessRunner, default_text_subprocess_runner
22
-
23
- _INLINE_PATTERN = re.compile(r"!`([^`]+)`")
24
-
25
-
26
- class PromptShellPermissionError(RuntimeError):
27
- """prompt 内嵌 shell 命令未通过 Bash 权限检查。"""
28
-
29
-
30
- class _ShellRunner(Protocol):
31
- def run_text(
32
- self,
33
- argv: list[str],
34
- *,
35
- cwd: str | Path | None = None,
36
- timeout_sec: float | None = None,
37
- ) -> tuple[int, str, str]: ...
38
-
39
-
40
- def _default_runner_call(
41
- runner: TextSubprocessRunner,
42
- command: str,
43
- *,
44
- cwd: str | Path | None,
45
- ) -> tuple[int, str, str]:
46
- completed = runner.run(
47
- ["bash", "-lc", command],
48
- cwd=cwd,
49
- timeout=30.0,
50
- capture_output=True,
51
- )
52
- stdout = completed.stdout or ""
53
- stderr = completed.stderr or ""
54
- if completed.returncode != 0 and not stdout.strip():
55
- return completed.returncode, "", stderr.strip() or f"exit {completed.returncode}"
56
- return completed.returncode, stdout, stderr
57
-
58
-
59
- def _assert_prompt_shell_permission(
60
- command: str,
61
- *,
62
- bash_allow_rules: Sequence[str] | None,
63
- cwd: str | Path | None,
64
- ) -> None:
65
- from langchain_agentx.tools.bash.tool import BashRuntimeTool
66
-
67
- store = AgentSessionStore("prompt-shell")
68
- if bash_allow_rules:
69
- set_command_bash_allow_rules(store, bash_allow_rules)
70
- workspace_root = str(cwd) if cwd is not None else None
71
- ctx = ToolExecutionContext(
72
- tool_name="Bash",
73
- tool_call_id="prompt-shell",
74
- input_args={"command": command},
75
- state={},
76
- session_store=store,
77
- workspace_root=workspace_root,
78
- cwd=workspace_root,
79
- )
80
- l1 = BashRuntimeTool().run_l1_permission_check({"command": command}, ctx)
81
- if l1.outcome == "deny":
82
- message = (l1.decision.message if l1.decision else None) or "Permission denied"
83
- raise PromptShellPermissionError(
84
- f'Shell command permission check failed for "!`{command}`": {message}',
85
- )
86
-
87
-
88
- async def execute_shell_commands_in_prompt(
89
- text: str,
90
- *,
91
- cwd: str | Path | None = None,
92
- runner: TextSubprocessRunner | None = None,
93
- shell_runner: Callable[[str], tuple[int, str, str]] | None = None,
94
- bash_allow_rules: Sequence[str] | None = None,
95
- check_permissions: bool = True,
96
- ) -> str:
97
- """将 text 中所有 !`command` 替换为命令 stdout(失败时内联 stderr/exit)。"""
98
- if "!`" not in text:
99
- return text
100
-
101
- text_runner = runner or default_text_subprocess_runner()
102
- result = text
103
-
104
- for match in list(_INLINE_PATTERN.finditer(text)):
105
- command = match.group(1).strip()
106
- if not command:
107
- continue
108
- if check_permissions and shell_runner is None:
109
- _assert_prompt_shell_permission(
110
- command,
111
- bash_allow_rules=bash_allow_rules,
112
- cwd=cwd,
113
- )
114
- if shell_runner is not None:
115
- _code, stdout, stderr = shell_runner(command)
116
- else:
117
- _code, stdout, stderr = _default_runner_call(text_runner, command, cwd=cwd)
118
- replacement = stdout.strip() if stdout.strip() else stderr.strip()
119
- if not replacement:
120
- replacement = "(no output)"
121
- result = result.replace(match.group(0), replacement, 1)
122
-
123
- return result
1
+ """
2
+ command/prompt_shell.py — Prompt 内嵌 shell 命令执行(CC executeShellCommandsInPrompt 对齐)。
3
+
4
+ 职责:
5
+ 解析并执行 prompt 中的 !`command` 占位,将输出内联替换;执行前走路由权限检查。
6
+
7
+ 链路位置:
8
+ 由 command/builtin/commit* 在展开 prompt 前调用。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from collections.abc import Callable, Sequence
15
+ from pathlib import Path
16
+ from typing import Protocol
17
+
18
+ from langchain_agentx.tool_runtime.models import ToolExecutionContext
19
+ from langchain_agentx.tool_runtime.permission_context import set_command_bash_allow_rules
20
+ from langchain_agentx.tool_runtime.session_store import AgentSessionStore
21
+ from langchain_agentx.utils.subprocess_text import TextSubprocessRunner, default_text_subprocess_runner
22
+
23
+ _INLINE_PATTERN = re.compile(r"!`([^`]+)`")
24
+
25
+
26
+ class PromptShellPermissionError(RuntimeError):
27
+ """prompt 内嵌 shell 命令未通过 Bash 权限检查。"""
28
+
29
+
30
+ class _ShellRunner(Protocol):
31
+ def run_text(
32
+ self,
33
+ argv: list[str],
34
+ *,
35
+ cwd: str | Path | None = None,
36
+ timeout_sec: float | None = None,
37
+ ) -> tuple[int, str, str]: ...
38
+
39
+
40
+ def _default_runner_call(
41
+ runner: TextSubprocessRunner,
42
+ command: str,
43
+ *,
44
+ cwd: str | Path | None,
45
+ ) -> tuple[int, str, str]:
46
+ completed = runner.run(
47
+ ["bash", "-lc", command],
48
+ cwd=cwd,
49
+ timeout=30.0,
50
+ capture_output=True,
51
+ )
52
+ stdout = completed.stdout or ""
53
+ stderr = completed.stderr or ""
54
+ if completed.returncode != 0 and not stdout.strip():
55
+ return completed.returncode, "", stderr.strip() or f"exit {completed.returncode}"
56
+ return completed.returncode, stdout, stderr
57
+
58
+
59
+ def _assert_prompt_shell_permission(
60
+ command: str,
61
+ *,
62
+ bash_allow_rules: Sequence[str] | None,
63
+ cwd: str | Path | None,
64
+ ) -> None:
65
+ from langchain_agentx.tools.bash.tool import BashRuntimeTool
66
+
67
+ store = AgentSessionStore("prompt-shell")
68
+ if bash_allow_rules:
69
+ set_command_bash_allow_rules(store, bash_allow_rules)
70
+ workspace_root = str(cwd) if cwd is not None else None
71
+ ctx = ToolExecutionContext(
72
+ tool_name="Bash",
73
+ tool_call_id="prompt-shell",
74
+ input_args={"command": command},
75
+ state={},
76
+ session_store=store,
77
+ workspace_root=workspace_root,
78
+ cwd=workspace_root,
79
+ )
80
+ l1 = BashRuntimeTool().run_l1_permission_check({"command": command}, ctx)
81
+ if l1.outcome == "deny":
82
+ message = (l1.decision.message if l1.decision else None) or "Permission denied"
83
+ raise PromptShellPermissionError(
84
+ f'Shell command permission check failed for "!`{command}`": {message}',
85
+ )
86
+
87
+
88
+ async def execute_shell_commands_in_prompt(
89
+ text: str,
90
+ *,
91
+ cwd: str | Path | None = None,
92
+ runner: TextSubprocessRunner | None = None,
93
+ shell_runner: Callable[[str], tuple[int, str, str]] | None = None,
94
+ bash_allow_rules: Sequence[str] | None = None,
95
+ check_permissions: bool = True,
96
+ ) -> str:
97
+ """将 text 中所有 !`command` 替换为命令 stdout(失败时内联 stderr/exit)。"""
98
+ if "!`" not in text:
99
+ return text
100
+
101
+ text_runner = runner or default_text_subprocess_runner()
102
+ result = text
103
+
104
+ for match in list(_INLINE_PATTERN.finditer(text)):
105
+ command = match.group(1).strip()
106
+ if not command:
107
+ continue
108
+ if check_permissions and shell_runner is None:
109
+ _assert_prompt_shell_permission(
110
+ command,
111
+ bash_allow_rules=bash_allow_rules,
112
+ cwd=cwd,
113
+ )
114
+ if shell_runner is not None:
115
+ _code, stdout, stderr = shell_runner(command)
116
+ else:
117
+ _code, stdout, stderr = _default_runner_call(text_runner, command, cwd=cwd)
118
+ replacement = stdout.strip() if stdout.strip() else stderr.strip()
119
+ if not replacement:
120
+ replacement = "(no output)"
121
+ result = result.replace(match.group(0), replacement, 1)
122
+
123
+ return result
@@ -36,6 +36,22 @@ class CommandRegistry:
36
36
  for alias in command.aliases:
37
37
  self._alias_to_name[self._normalize(alias)] = normalized_name
38
38
 
39
+ def register_many(
40
+ self,
41
+ commands: Iterable[CommandDefinition],
42
+ *,
43
+ skip_existing: bool = True,
44
+ ) -> list[str]:
45
+ """批量注册。默认跳过已有名(内置优先于 MCP)。"""
46
+ registered: list[str] = []
47
+ for command in commands:
48
+ normalized_name = self._normalize(command.name)
49
+ if skip_existing and normalized_name in self._commands:
50
+ continue
51
+ self.register(command)
52
+ registered.append(normalized_name)
53
+ return registered
54
+
39
55
  def register_plugin(
40
56
  self,
41
57
  commands: Iterable[CommandDefinition],
@@ -313,6 +313,12 @@ class AgentLoopConfig:
313
313
  )
314
314
  model_retry: ModelRetrySettings = field(default_factory=ModelRetrySettings)
315
315
 
316
+ # MCP:默认空,不 connect / 不扫盘。仅显式 specs / controller / plugin 片段时 Loop 接线(S2)。
317
+ mcp_servers: tuple[Any, ...] = ()
318
+ mcp_plugins: tuple[Any, ...] = ()
319
+ mcp_skills_enabled: bool = True
320
+ mcp_elicitation_auto_accept: bool = False
321
+
316
322
  # -----------------------------------------------------------------------
317
323
  # 便捷构造器
318
324
  # -----------------------------------------------------------------------
@@ -394,6 +400,9 @@ class LoopServices:
394
400
  queued_command_providers: Sequence[Any] = field(default_factory=tuple)
395
401
  active_files_getter: Callable[[], list[str]] | None = None
396
402
 
403
+ mcp_controller: Any = None # McpRuntimeController;默认 None 零成本
404
+ command_registry: Any = None # CommandRegistry;MCP prompts register_many
405
+
397
406
 
398
407
  __all__ = [
399
408
  "PromptConfig",
@@ -317,6 +317,7 @@ def _flatten_config_for_runtime(config: AgentLoopConfig) -> _FlatLoopConfig:
317
317
  trace_enabled=config.trace.enabled,
318
318
  trace_persist=config.trace.persist,
319
319
  headless=config.headless,
320
+ container_type=config.container_type,
320
321
  model_profile=config.model_profile,
321
322
  model_profile_registry=config.model_profile_registry,
322
323
  compact_buffer_tokens=config.compact_buffer_tokens,
@@ -549,6 +550,15 @@ class LoopGraphBuilder:
549
550
  available_tools=self._loader_tools,
550
551
  ).build_sections()
551
552
  )
553
+ if (
554
+ self._loop_loader is not None
555
+ and getattr(self._loop_loader, "mcp_controller", None) is not None
556
+ ):
557
+ from ...loop.prompt.mcp_instructions import mcp_instructions_section
558
+
559
+ self._dynamic_sections.append(
560
+ mcp_instructions_section(self._loop_loader.mcp_controller)
561
+ )
552
562
  system_message = self._build_system_message(
553
563
  base_prompt=base_prompt,
554
564
  append_system_prompt=append_system_prompt,
@@ -783,6 +793,10 @@ class LoopGraphBuilder:
783
793
  )
784
794
  continue
785
795
  self._loop_loader.register(tool)
796
+ self._mcp_bind_session_store = session_store
797
+ self._mcp_bind_model = model
798
+ self._mcp_bind_schema_ns = schema_cache_namespace
799
+ self._bind_optional_mcp()
786
800
  self._runtime_tools = self._loop_loader.registry.list()
787
801
  from ...loop.prompt.tool_aware_sections import collect_enabled_tool_names
788
802
 
@@ -799,6 +813,62 @@ class LoopGraphBuilder:
799
813
  if self._skill_runtime_tool is None and rt.__class__.__name__ == "SkillRuntimeTool":
800
814
  self._skill_runtime_tool = rt
801
815
 
816
+ def _bind_optional_mcp(self) -> None:
817
+ if self._loop_loader is None:
818
+ return
819
+ controller = self._services.mcp_controller
820
+ specs = self._config.mcp_servers
821
+ plugins = self._config.mcp_plugins
822
+ if controller is None and not specs and not plugins:
823
+ return
824
+ self._loop_loader.set_mcp_skills_enabled(
825
+ bool(getattr(self._config, "mcp_skills_enabled", True))
826
+ )
827
+ for tool in self._loop_loader.registry.list():
828
+ if tool.__class__.__name__ != "SkillRuntimeTool":
829
+ continue
830
+ catalog = getattr(tool, "_catalog", None)
831
+ if catalog is not None:
832
+ self._loop_loader.set_mcp_skill_catalog(catalog)
833
+ break
834
+ self._loop_loader.attach_mcp(
835
+ controller=controller,
836
+ specs=specs or None,
837
+ plugins=plugins or None,
838
+ schema_cache_namespace=getattr(self, "_mcp_bind_schema_ns", None),
839
+ after_refresh=self._rebuild_views_after_mcp_refresh,
840
+ command_registry=self._services.command_registry,
841
+ )
842
+ self._apply_mcp_elicitation_auto_accept()
843
+
844
+ def _apply_mcp_elicitation_auto_accept(self) -> None:
845
+ if self._loop_loader is None:
846
+ return
847
+ if not bool(getattr(self._config, "mcp_elicitation_auto_accept", False)):
848
+ return
849
+ ctrl = self._loop_loader.mcp_controller
850
+ if ctrl is None:
851
+ return
852
+ from langchain_agentx.mcp.elicitation import AcceptingElicitationInterceptor
853
+
854
+ ctrl.manager.elicitation.set_interceptor(AcceptingElicitationInterceptor())
855
+
856
+ def _rebuild_views_after_mcp_refresh(self) -> None:
857
+ if self._loop_loader is None:
858
+ return
859
+ from ...loop.prompt.tool_aware_sections import collect_enabled_tool_names
860
+
861
+ self._runtime_tools = self._loop_loader.registry.list()
862
+ self._enabled_tool_names = collect_enabled_tool_names(self._runtime_tools)
863
+ self._tool_prompt_ctx = self._build_tool_prompt_context(
864
+ session_store=getattr(self, "_mcp_bind_session_store", None),
865
+ model=getattr(self, "_mcp_bind_model", None),
866
+ schema_cache_namespace=getattr(self, "_mcp_bind_schema_ns", None),
867
+ )
868
+ self._loader_tools = self._loop_loader.registry.to_langchain_tools(
869
+ prompt_ctx=self._tool_prompt_ctx,
870
+ )
871
+
802
872
  def refresh_permission_settings(
803
873
  self,
804
874
  *,
@@ -0,0 +1,40 @@
1
+ """MCP server instructions → SystemPromptSection。
2
+
3
+ 职责:
4
+ 截断 2048;只经 ModelRequest.system_message 动态段,不写入 state messages。
5
+
6
+ 链路位置:
7
+ LoopGraphBuilder._prepare_compile_context 在 MCP 显式启用时追加。
8
+
9
+ 当前裁剪:
10
+ 无进度条;无 CC UI 折叠。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any
16
+
17
+ from langchain_agentx.loop.prompt.sections import SystemPromptSection, section
18
+ from langchain_agentx.protocol.mcp.truncate import truncate_for_model
19
+
20
+
21
+ def format_mcp_instructions(snapshots: Any) -> str | None:
22
+ lines: list[str] = []
23
+ for item in snapshots or ():
24
+ text = getattr(item, "instructions", None)
25
+ if not text:
26
+ continue
27
+ name = getattr(item, "name", "") or "mcp"
28
+ lines.append(f"## MCP server `{name}`\n{truncate_for_model(str(text))}")
29
+ if not lines:
30
+ return None
31
+ return "# MCP server instructions\n\n" + "\n\n".join(lines)
32
+
33
+
34
+ def mcp_instructions_section(controller: Any) -> SystemPromptSection:
35
+ def _compute() -> str | None:
36
+ status = getattr(controller, "status", None)
37
+ snapshots = status() if callable(status) else ()
38
+ return format_mcp_instructions(snapshots)
39
+
40
+ return section("mcp_server_instructions", _compute)