agentshim 0.9.0__tar.gz → 0.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {agentshim-0.9.0 → agentshim-0.10.0}/CHANGELOG.md +33 -0
  2. {agentshim-0.9.0 → agentshim-0.10.0}/PKG-INFO +2 -1
  3. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/__init__.py +3 -1
  4. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/agent.py +32 -6
  5. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/__init__.py +2 -0
  6. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/profile.py +18 -0
  7. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/provider.py +8 -1
  8. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/provider.py +19 -0
  9. agentshim-0.10.0/agentshim/providers/codex/mcp_scope.py +93 -0
  10. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/provider.py +22 -2
  11. {agentshim-0.9.0 → agentshim-0.10.0}/pyproject.toml +4 -2
  12. {agentshim-0.9.0 → agentshim-0.10.0}/.gitignore +0 -0
  13. {agentshim-0.9.0 → agentshim-0.10.0}/README.md +0 -0
  14. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/_files.py +0 -0
  15. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/env.py +0 -0
  16. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/errors.py +0 -0
  17. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/events.py +0 -0
  18. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/mcp.py +0 -0
  19. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/pricing.py +0 -0
  20. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/schema.py +0 -0
  21. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/skills.py +0 -0
  22. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/stream.py +0 -0
  23. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/turn.py +0 -0
  24. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/core/usage.py +0 -0
  25. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/execution/__init__.py +0 -0
  26. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/execution/executor.py +0 -0
  27. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/execution/host.py +0 -0
  28. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/execution/transform.py +0 -0
  29. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/__init__.py +0 -0
  30. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/__init__.py +0 -0
  31. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/events.py +0 -0
  32. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
  33. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
  34. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/parser.py +0 -0
  35. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/sandbox.py +0 -0
  36. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/scripted.py +0 -0
  37. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/claude/user_hooks.py +0 -0
  38. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/__init__.py +0 -0
  39. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/_toml.py +0 -0
  40. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/events.py +0 -0
  41. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/parser.py +0 -0
  42. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/rules.py +0 -0
  43. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/sandbox.py +0 -0
  44. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/scripted.py +0 -0
  45. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/codex/skills.py +0 -0
  46. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/copilot/__init__.py +0 -0
  47. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/copilot/events.py +0 -0
  48. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/copilot/parser.py +0 -0
  49. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/copilot/provider.py +0 -0
  50. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/copilot/scripted.py +0 -0
  51. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/gemini/__init__.py +0 -0
  52. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/gemini/events.py +0 -0
  53. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/gemini/parser.py +0 -0
  54. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/gemini/provider.py +0 -0
  55. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/gemini/scripted.py +0 -0
  56. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/opencode/__init__.py +0 -0
  57. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/opencode/events.py +0 -0
  58. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/opencode/parser.py +0 -0
  59. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/opencode/provider.py +0 -0
  60. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/providers/opencode/scripted.py +0 -0
  61. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/py.typed +0 -0
  62. {agentshim-0.9.0 → agentshim-0.10.0}/agentshim/testing/__init__.py +0 -0
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 (2026-10-03)
4
+
5
+ MCP isolation. Additive: the default scope keeps today's behaviour. Adds a
6
+ runtime dependency on `tomli` for Python 3.10 only (Codex config parsing).
7
+
8
+ ### Added
9
+
10
+ - `McpScope` (`ALL`, `SESSION`) and `start_session(mcp_scope=...)` /
11
+ `run(mcp_scope=...)`. `SESSION` connects the CLI only to the servers in
12
+ `TurnRequest.mcp_servers`; a turn given none sees no MCP server. It is a
13
+ separate option from `SkillScope` because a provider may enforce one and not
14
+ the other; each is refused independently.
15
+ - `ProviderProfile.mcp_scopes` declares the scopes a provider enforces;
16
+ asking for another raises `ProviderCapabilityError` from `start_session`.
17
+ - `ArgvContext.mcp_scope` and `ArgvContext.mcp_servers` carry the scope and
18
+ the turn's servers to `build_argv`.
19
+ - Claude Code: `SESSION` passes `--strict-mcp-config` plus the turn's servers
20
+ inline as `--mcp-config`. This drops user and project (`.mcp.json`) servers,
21
+ plugin servers and claude.ai account connectors.
22
+ - Codex: `SESSION` sets `enabled = false` on every `mcp_servers` entry found
23
+ in `$CODEX_HOME/config.toml`, `/etc/codex/config.toml` and
24
+ `<cwd>/.codex/config.toml` that the turn was not given, and sets
25
+ `features.plugins=false` and `features.apps=false` (plugin servers and the
26
+ account's ChatGPT apps).
27
+ - Gemini, opencode and Copilot support `ALL` only.
28
+ - Live e2e: a server seeded into the CLI's default configuration is reachable
29
+ under `ALL` and not under `SESSION` (`tests/e2e/test_mcp_scope_e2e.py`).
30
+
31
+ ### Changed
32
+
33
+ - `parse_mcp_servers` ignores `enabled=false` overrides: they disable a
34
+ configured server and do not define one.
35
+
3
36
  ## 0.9.0 (2026-10-03)
4
37
 
5
38
  Skill isolation. Additive: the default scope keeps today's behaviour.
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentshim
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: Provider-agnostic coding-agent CLI shims
5
5
  Requires-Python: >=3.10
6
+ Requires-Dist: tomli>=2; python_full_version < '3.11'
6
7
  Provides-Extra: test
7
8
  Requires-Dist: pytest>=8.0.0; extra == 'test'
8
9
  Description-Content-Type: text/markdown
@@ -27,6 +27,7 @@ from .core import (
27
27
  McpConfigError,
28
28
  McpInstallation,
29
29
  McpMechanism,
30
+ McpScope,
30
31
  McpServer,
31
32
  McpTransport,
32
33
  ModelPricing,
@@ -97,7 +98,7 @@ from .providers.copilot import CopilotProvider
97
98
  from .providers.gemini import GeminiProvider
98
99
  from .providers.opencode import OpencodeProvider
99
100
 
100
- __version__ = "0.9.0"
101
+ __version__ = "0.10.0"
101
102
 
102
103
  __all__ = [
103
104
  "AgentEvent",
@@ -134,6 +135,7 @@ __all__ = [
134
135
  "McpConfigError",
135
136
  "McpInstallation",
136
137
  "McpMechanism",
138
+ "McpScope",
137
139
  "McpServer",
138
140
  "McpTransport",
139
141
  "ModelPricing",
@@ -23,7 +23,13 @@ from agentshim.core.errors import (
23
23
  SessionResumeError,
24
24
  )
25
25
  from agentshim.core.events import RunFinished, RunStarted, compose_event_handlers
26
- from agentshim.core.profile import McpMechanism, OutputSchemaStyle, SchemaDialect, SkillScope
26
+ from agentshim.core.profile import (
27
+ McpMechanism,
28
+ McpScope,
29
+ OutputSchemaStyle,
30
+ SchemaDialect,
31
+ SkillScope,
32
+ )
27
33
  from agentshim.core.provider import ArgvContext
28
34
  from agentshim.core.schema import compact_json, dialect_problems, materialize
29
35
  from agentshim.core.skills import SkillTracker
@@ -115,13 +121,20 @@ class CliAgent:
115
121
  timeout: float | None = None,
116
122
  session_id: str | None = None,
117
123
  skill_scope: SkillScope = SkillScope.ALL,
124
+ mcp_scope: McpScope = McpScope.ALL,
118
125
  ) -> AgentSession:
119
126
  """Open a conversation whose turns resume one another.
120
127
 
121
- ``skill_scope`` is fixed for the conversation (see ``AgentSession``).
128
+ ``skill_scope`` and ``mcp_scope`` are fixed for the conversation (see
129
+ ``AgentSession``).
122
130
  """
123
131
  return AgentSession(
124
- self, cwd=cwd, timeout=timeout, session_id=session_id, skill_scope=skill_scope
132
+ self,
133
+ cwd=cwd,
134
+ timeout=timeout,
135
+ session_id=session_id,
136
+ skill_scope=skill_scope,
137
+ mcp_scope=mcp_scope,
125
138
  )
126
139
 
127
140
  def run(
@@ -131,9 +144,12 @@ class CliAgent:
131
144
  cwd: str | None = None,
132
145
  timeout: float | None = None,
133
146
  skill_scope: SkillScope = SkillScope.ALL,
147
+ mcp_scope: McpScope = McpScope.ALL,
134
148
  ) -> TurnResult:
135
149
  """Run one turn in a throwaway session."""
136
- session = self.start_session(cwd=cwd, timeout=timeout, skill_scope=skill_scope)
150
+ session = self.start_session(
151
+ cwd=cwd, timeout=timeout, skill_scope=skill_scope, mcp_scope=mcp_scope
152
+ )
137
153
  return session.turn(request)
138
154
 
139
155
 
@@ -144,7 +160,7 @@ def _discard(message: str) -> None:
144
160
  class AgentSession:
145
161
  """A provider conversation. One turn at a time; ``cancel`` is thread-safe."""
146
162
 
147
- def __init__(
163
+ def __init__( # noqa: PLR0913 - one keyword per independent session option
148
164
  self,
149
165
  agent: CliAgent,
150
166
  *,
@@ -152,6 +168,7 @@ class AgentSession:
152
168
  timeout: float | None = None,
153
169
  session_id: str | None = None,
154
170
  skill_scope: SkillScope = SkillScope.ALL,
171
+ mcp_scope: McpScope = McpScope.ALL,
155
172
  ) -> None:
156
173
  """Bind a conversation to one agent, with per-turn defaults.
157
174
 
@@ -162,12 +179,19 @@ class AgentSession:
162
179
  first turn resumes rather than starts fresh. ``skill_scope`` limits
163
180
  which skills every turn's CLI may discover; a scope the provider's
164
181
  ``profile.skill_scopes`` does not list raises
165
- ``ProviderCapabilityError`` here, before any turn runs.
182
+ ``ProviderCapabilityError`` here, before any turn runs. ``mcp_scope``
183
+ limits which MCP servers every turn's CLI may connect to
184
+ (``McpScope.SESSION``: only those in ``TurnRequest.mcp_servers``) and
185
+ is checked against ``profile.mcp_scopes`` the same way.
166
186
  """
167
187
  if skill_scope not in agent.profile.skill_scopes:
168
188
  msg = f"{agent.profile.name} cannot limit skills to scope {skill_scope.value!r}"
169
189
  raise ProviderCapabilityError(msg)
190
+ if mcp_scope not in agent.profile.mcp_scopes:
191
+ msg = f"{agent.profile.name} cannot limit MCP servers to scope {mcp_scope.value!r}"
192
+ raise ProviderCapabilityError(msg)
170
193
  self.skill_scope = skill_scope
194
+ self.mcp_scope = mcp_scope
171
195
  self._agent = agent
172
196
  self._cwd = cwd
173
197
  self._timeout = timeout
@@ -277,6 +301,8 @@ class AgentSession:
277
301
  extra_args=req.extra_args,
278
302
  cwd=cwd,
279
303
  skill_scope=self.skill_scope,
304
+ mcp_scope=self.mcp_scope,
305
+ mcp_servers=tuple(req.mcp_servers),
280
306
  )
281
307
  )
282
308
  command = CommandRequest(argv=argv, stdin=req.prompt, cwd=cwd, env=env, timeout=timeout)
@@ -55,6 +55,7 @@ from .mcp import (
55
55
  from .pricing import ModelPricing, PricingTable, cost_usd, default_pricing, price_for
56
56
  from .profile import (
57
57
  McpMechanism,
58
+ McpScope,
58
59
  OutputSchemaStyle,
59
60
  ProviderProfile,
60
61
  SchemaDialect,
@@ -88,6 +89,7 @@ __all__ = [
88
89
  "McpConfigError",
89
90
  "McpInstallation",
90
91
  "McpMechanism",
92
+ "McpScope",
91
93
  "McpServer",
92
94
  "McpTransport",
93
95
  "ModelPricing",
@@ -77,6 +77,22 @@ class SkillScope(Enum):
77
77
  PROJECT = "project"
78
78
 
79
79
 
80
+ class McpScope(Enum):
81
+ """Which MCP servers a session's CLI may connect to.
82
+
83
+ ``ALL``: whatever the CLI finds by default, including the user's and the
84
+ project's own MCP configuration, plugin servers and account-level
85
+ connectors. ``SESSION``: only the servers the session was given through
86
+ ``TurnRequest.mcp_servers``; a session given none sees no MCP server at
87
+ all. A provider declares the scopes it can enforce in
88
+ ``ProviderProfile.mcp_scopes``; asking for another one is an error, never
89
+ a silent ``ALL``.
90
+ """
91
+
92
+ ALL = "all"
93
+ SESSION = "session"
94
+
95
+
80
96
  def _no_container_env() -> Mapping[str, str]:
81
97
  """Empty read-only default: a frozen spec must not carry a mutable one.
82
98
 
@@ -127,3 +143,5 @@ class ProviderProfile:
127
143
  skill_invocation: SkillSignal = SkillSignal.NONE
128
144
  #: The ``SkillScope`` values a session on this provider may request.
129
145
  skill_scopes: frozenset[SkillScope] = frozenset({SkillScope.ALL})
146
+ #: The ``McpScope`` values a session on this provider may request.
147
+ mcp_scopes: frozenset[McpScope] = frozenset({McpScope.ALL})
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from dataclasses import dataclass, field
6
6
  from typing import TYPE_CHECKING, Any, Protocol
7
7
 
8
- from .profile import SkillScope
8
+ from .profile import McpScope, SkillScope
9
9
  from .usage import ProviderUsage
10
10
 
11
11
  if TYPE_CHECKING:
@@ -41,6 +41,13 @@ class ArgvContext:
41
41
  #: Which skills the CLI may discover; the session has already checked
42
42
  #: that the provider's profile supports it.
43
43
  skill_scope: SkillScope = SkillScope.ALL
44
+ #: Which MCP servers the CLI may connect to; the session has already
45
+ #: checked that the provider's profile supports it.
46
+ mcp_scope: McpScope = McpScope.ALL
47
+ #: The servers the turn was given (the ones ``install_mcp`` installed).
48
+ #: A provider enforcing ``McpScope.SESSION`` renders them itself when its
49
+ #: installed config file is not enough to exclude the user's own.
50
+ mcp_servers: Sequence[McpServer] = ()
44
51
 
45
52
 
46
53
  @dataclass(frozen=True)
@@ -9,6 +9,7 @@ from agentshim.core.errors import ProviderCapabilityError, SessionResumeError
9
9
  from agentshim.core.mcp import HttpMcpServer, NoopInstallation, StdioMcpServer, install_config_file
10
10
  from agentshim.core.profile import (
11
11
  McpMechanism,
12
+ McpScope,
12
13
  OutputSchemaStyle,
13
14
  ProviderProfile,
14
15
  SchemaDialect,
@@ -43,6 +44,12 @@ MCP_SERVER_KEY = "mcpServers"
43
44
  #: account connectors are not user settings and stay.
44
45
  PROJECT_SETTING_SOURCES = ("--setting-sources", "project,local")
45
46
 
47
+ #: ``McpScope.SESSION``: ``--strict-mcp-config`` makes ``--mcp-config`` the
48
+ #: only source of MCP servers. It drops the user's and the project's
49
+ #: (``.mcp.json``) servers, plugin servers and claude.ai account connectors,
50
+ #: so the servers the turn was given are passed inline as ``--mcp-config``.
51
+ STRICT_MCP_FLAG = "--strict-mcp-config"
52
+
46
53
  PROFILE = ProviderProfile(
47
54
  name="claude",
48
55
  display_name="Claude Code",
@@ -67,6 +74,7 @@ PROFILE = ProviderProfile(
67
74
  skill_discovery=SkillSignal.STRUCTURED,
68
75
  skill_invocation=SkillSignal.STRUCTURED,
69
76
  skill_scopes=frozenset({SkillScope.ALL, SkillScope.PROJECT}),
77
+ mcp_scopes=frozenset({McpScope.ALL, McpScope.SESSION}),
70
78
  container_install=(
71
79
  "apt-get update && apt-get install -y --no-install-recommends curl ca-certificates",
72
80
  "curl -fsSL https://claude.ai/install.sh | bash",
@@ -141,6 +149,8 @@ class ClaudeProvider:
141
149
  settings = build_settings(self.sandbox, hooks=self.hooks)
142
150
  if settings:
143
151
  argv += ["--settings", json.dumps(settings)]
152
+ if ctx.mcp_scope is McpScope.SESSION:
153
+ argv += _session_mcp_argv(ctx.mcp_servers)
144
154
  argv += list(ctx.mcp_argv)
145
155
  argv += list(ctx.extra_args)
146
156
  return argv
@@ -193,6 +203,15 @@ def _resumed_session_id(argv: Sequence[str]) -> str | None:
193
203
  return args[index + 1]
194
204
 
195
205
 
206
+ def _session_mcp_argv(servers: Sequence[McpServer]) -> list[str]:
207
+ """Flags that leave Claude Code exactly *servers* and nothing else."""
208
+ argv = [STRICT_MCP_FLAG]
209
+ if servers:
210
+ config = {MCP_SERVER_KEY: {server.name: mcp_entry(server) for server in servers}}
211
+ argv += ["--mcp-config", json.dumps(config)]
212
+ return argv
213
+
214
+
196
215
  def mcp_entry(server: McpServer) -> dict[str, Any]:
197
216
  """Render one server the way ``.mcp.json`` describes it."""
198
217
  if server.startup_timeout_s is not None:
@@ -0,0 +1,93 @@
1
+ """Codex MCP servers: hide every one the session was not given.
2
+
3
+ Codex merges MCP servers from several layers: ``$CODEX_HOME/config.toml``,
4
+ system config, a trusted workspace's ``.codex/config.toml``, installed
5
+ plugins, and the account's ChatGPT apps (the ``codex_apps`` server). A
6
+ ``--config mcp_servers={}`` override does not replace the merged table, so
7
+ each configured server is switched off by name (``enabled = false``), and
8
+ plugins and apps are switched off as features.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import sys
15
+ from typing import TYPE_CHECKING, Any, cast
16
+
17
+ from ._toml import toml_str
18
+
19
+ if sys.version_info >= (3, 11):
20
+ import tomllib
21
+ else: # pragma: no cover - exercised on the 3.10 CI leg
22
+ import tomli as tomllib # pyright: ignore[reportMissingImports]
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Collection, Iterable, Mapping
26
+
27
+ _SYSTEM_CONFIG = "/etc/codex/config.toml"
28
+ _BARE_KEY = frozenset("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-")
29
+
30
+ #: Features that bring in MCP servers on their own: plugins (each may bundle
31
+ #: servers) and apps (the account's ChatGPT connectors).
32
+ _MCP_FEATURES = ("plugins", "apps")
33
+
34
+
35
+ def configured_server_keys(env: Mapping[str, str], cwd: str | None) -> list[str]:
36
+ """Every ``mcp_servers`` key the config files Codex would read define.
37
+
38
+ Reads the user's ``$CODEX_HOME/config.toml`` (default ``~/.codex``), the
39
+ system config, and ``<cwd>/.codex/config.toml``. A file that is missing or
40
+ not valid TOML contributes nothing (Codex reports a broken config itself).
41
+ The scan runs where agentshim runs, so a CLI in a container is only
42
+ covered when the container shares these files.
43
+ """
44
+ home = env.get("HOME") or os.path.expanduser("~") # noqa: PTH111 - a str contract, not a Path
45
+ codex_home = env.get("CODEX_HOME") or os.path.join(home, ".codex") # noqa: PTH118
46
+ paths = [os.path.join(codex_home, "config.toml"), _SYSTEM_CONFIG] # noqa: PTH118
47
+ if cwd is not None:
48
+ paths.append(os.path.join(cwd, ".codex", "config.toml")) # noqa: PTH118
49
+ keys: list[str] = []
50
+ for path in paths:
51
+ keys += _server_keys(path)
52
+ return list(dict.fromkeys(keys))
53
+
54
+
55
+ def _server_keys(path: str) -> Iterable[str]:
56
+ config = _read_toml(path)
57
+ servers: object = config.get("mcp_servers")
58
+ if not isinstance(servers, dict):
59
+ return []
60
+ return [str(key) for key in cast("dict[object, object]", servers)]
61
+
62
+
63
+ def _read_toml(path: str) -> dict[str, Any]:
64
+ try:
65
+ with open(path, "rb") as handle: # noqa: PTH123 - a str contract, not a Path
66
+ # pyright analyzes the 3.10 leg, where the optional ``tomli`` has no
67
+ # stubs in the 3.12 environment: the types are known at runtime.
68
+ return tomllib.load(handle) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
69
+ except (OSError, tomllib.TOMLDecodeError): # pyright: ignore[reportUnknownMemberType]
70
+ return {}
71
+
72
+
73
+ def _table_key(key: str) -> str:
74
+ return key if key and set(key) <= _BARE_KEY else toml_str(key)
75
+
76
+
77
+ def session_scope_overrides(
78
+ env: Mapping[str, str], cwd: str | None, given_keys: Collection[str]
79
+ ) -> list[str]:
80
+ """``--config`` flags that leave Codex only the servers in *given_keys*.
81
+
82
+ *given_keys* are the ``mcp_servers`` keys the turn's own overrides define;
83
+ they are not disabled. A given server that shares a key with a configured
84
+ one is merged field by field with it by Codex, so give session servers
85
+ names the user's config does not use.
86
+ """
87
+ flags: list[str] = []
88
+ for key in configured_server_keys(env, cwd):
89
+ if key not in given_keys:
90
+ flags += ["--config", f"mcp_servers.{_table_key(key)}.enabled=false"]
91
+ for feature in _MCP_FEATURES:
92
+ flags += ["--config", f"features.{feature}=false"]
93
+ return flags
@@ -10,6 +10,7 @@ from agentshim.core.errors import ProviderCapabilityError, SessionResumeError
10
10
  from agentshim.core.mcp import FlagsInstallation, HttpMcpServer, NoopInstallation
11
11
  from agentshim.core.profile import (
12
12
  McpMechanism,
13
+ McpScope,
13
14
  OutputSchemaStyle,
14
15
  ProviderProfile,
15
16
  SchemaDialect,
@@ -18,6 +19,7 @@ from agentshim.core.profile import (
18
19
  )
19
20
 
20
21
  from ._toml import toml_array, toml_str, unescape_toml
22
+ from .mcp_scope import session_scope_overrides
21
23
  from .parser import CodexStreamParser
22
24
  from .rules import RULES_FILENAME
23
25
  from .sandbox import CodexSandboxConfig, resolve_sandbox, sandbox_overrides
@@ -82,6 +84,7 @@ PROFILE = ProviderProfile(
82
84
  # of a ``SKILL.md`` (``skills.py``).
83
85
  skill_invocation=SkillSignal.INFERRED,
84
86
  skill_scopes=frozenset({SkillScope.ALL, SkillScope.PROJECT}),
87
+ mcp_scopes=frozenset({McpScope.ALL, McpScope.SESSION}),
85
88
  container_install=(_NODE_INSTALL, _CODEX_INSTALL),
86
89
  # Documented Codex CLI variable that relocates ~/.codex (``codex --help``:
87
90
  # "Layer $CODEX_HOME/<name>.config.toml on top of the base user config";
@@ -133,8 +136,7 @@ class CodexProvider:
133
136
  if ctx.model:
134
137
  argv += ["--model", ctx.model]
135
138
  argv += _shell_path_config(ctx.env)
136
- if ctx.skill_scope is SkillScope.PROJECT:
137
- argv += project_scope_overrides(ctx.env)
139
+ argv += _scope_overrides(ctx)
138
140
  if ctx.reasoning_effort:
139
141
  argv += ["--config", f"model_reasoning_effort={toml_str(ctx.reasoning_effort)}"]
140
142
  argv += list(ctx.mcp_argv)
@@ -270,6 +272,21 @@ def _shell_path_config(env: Mapping[str, str]) -> list[str]:
270
272
  return ["--config", f"shell_environment_policy.set.PATH={toml_str(path)}"]
271
273
 
272
274
 
275
+ def _scope_overrides(ctx: ArgvContext) -> list[str]:
276
+ """The ``--config`` flags the session's skill and MCP scopes call for.
277
+
278
+ Both scopes switch ``features.plugins`` off; the flag is emitted once.
279
+ """
280
+ flags: list[str] = []
281
+ if ctx.skill_scope is SkillScope.PROJECT:
282
+ flags += project_scope_overrides(ctx.env)
283
+ if ctx.mcp_scope is McpScope.SESSION:
284
+ given = {server.name.replace("-", "_") for server in ctx.mcp_servers}
285
+ flags += session_scope_overrides(ctx.env, ctx.cwd, given)
286
+ pairs = [flags[i : i + 2] for i in range(0, len(flags), 2)]
287
+ return [item for pair in dict.fromkeys(map(tuple, pairs)) for item in pair]
288
+
289
+
273
290
  def _server_flags(server: McpServer) -> list[str]:
274
291
  """Render one MCP server as dotted-path TOML overrides.
275
292
 
@@ -338,6 +355,9 @@ def parse_mcp_servers(argv: Sequence[str]) -> dict[str, dict[str, Any]]:
338
355
  servers: dict[str, dict[str, Any]] = {}
339
356
  for key, field, raw_value in _mcp_config_overrides(argv):
340
357
  _apply_mcp_override(servers.setdefault(key, {}), field, raw_value)
358
+ # ``McpScope.SESSION`` also emits ``enabled=false`` overrides for servers
359
+ # the user configured; those define no server, so they are not reported.
360
+ servers = {key: entry for key, entry in servers.items() if "command" in entry or "url" in entry}
341
361
  for entry in servers.values():
342
362
  if "url" not in entry:
343
363
  entry.setdefault("args", [])
@@ -1,10 +1,12 @@
1
1
  [project]
2
2
  name = "agentshim"
3
- version = "0.9.0"
3
+ version = "0.10.0"
4
4
  description = "Provider-agnostic coding-agent CLI shims"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
- dependencies = []
7
+ dependencies = [
8
+ "tomli>=2 ; python_full_version < '3.11'",
9
+ ]
8
10
 
9
11
  [project.optional-dependencies]
10
12
  test = [
File without changes
File without changes
File without changes