codex-cli-mcp-slim 0.1.0__tar.gz → 0.1.1__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.
- {codex_cli_mcp_slim-0.1.0/src/codex_cli_mcp_slim.egg-info → codex_cli_mcp_slim-0.1.1}/PKG-INFO +27 -8
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/README.md +26 -7
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/pyproject.toml +1 -1
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/src/codex_cli_mcp_slim/server.py +138 -15
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1/src/codex_cli_mcp_slim.egg-info}/PKG-INFO +27 -8
- codex_cli_mcp_slim-0.1.1/tests/test_basic.py +346 -0
- codex_cli_mcp_slim-0.1.0/tests/test_basic.py +0 -123
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/LICENSE +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/setup.cfg +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/src/codex_cli_mcp_slim/__init__.py +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/src/codex_cli_mcp_slim/py.typed +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/src/codex_cli_mcp_slim.egg-info/SOURCES.txt +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/src/codex_cli_mcp_slim.egg-info/dependency_links.txt +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/src/codex_cli_mcp_slim.egg-info/entry_points.txt +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/src/codex_cli_mcp_slim.egg-info/requires.txt +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/src/codex_cli_mcp_slim.egg-info/top_level.txt +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/tests/test_call_tool.py +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/tests/test_subprocess_stdin_isolation.py +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/tests/test_timeout_handling.py +0 -0
{codex_cli_mcp_slim-0.1.0/src/codex_cli_mcp_slim.egg-info → codex_cli_mcp_slim-0.1.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-cli-mcp-slim
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: A thin, auditable MCP server wrapping the Codex CLI (codex exec). Same tools as the deprecated `codex mcp-server`; forward-compatible with future CLI flags via extra_args passthrough.
|
|
5
5
|
Author-email: tksfjt1024 <tksfjt1024@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -171,7 +171,9 @@ After:
|
|
|
171
171
|
Parameter names differ from the old server where `codex exec` names the flag
|
|
172
172
|
differently: `cwd` is now `cd` (the `-C/--cd` flag), and `codex-reply` takes
|
|
173
173
|
`thread_id` instead of `threadId`. The result's `structuredContent` field keeps
|
|
174
|
-
the shape the old server returned, `{"threadId": ..., "content": ...}`.
|
|
174
|
+
the shape the old server returned, `{"threadId": ..., "content": ...}`. It
|
|
175
|
+
gains an `ignoredParameters` key only when the server's own flags overrode a
|
|
176
|
+
per-call parameter (see [Server-level flags](#server-level-flags)).
|
|
175
177
|
|
|
176
178
|
### Other MCP clients
|
|
177
179
|
|
|
@@ -207,11 +209,25 @@ in reasoning effort look like this:
|
|
|
207
209
|
|
|
208
210
|
A call to `codex-high` runs
|
|
209
211
|
`codex exec -c model_reasoning_effort=high [per-call flags] --json -`. Per-call
|
|
210
|
-
flags come after the server-level ones
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
flags come after the server-level ones. `-c` and `--add-dir` may repeat. For
|
|
213
|
+
`-c` the last one wins, so a per-call `config` entry overrides a server-level
|
|
214
|
+
`-c`.
|
|
215
|
+
|
|
216
|
+
The other typed flags (`-C`, `-m`, `--sandbox`, `-p`, `--ephemeral`,
|
|
217
|
+
`--skip-git-repo-check`) may appear only once: `codex` exits with "cannot be used
|
|
218
|
+
multiple times" on a second one. When the server's command line already sets one
|
|
219
|
+
of them, in any spelling (for example `-C DIR`, `-CDIR`, `--cd=DIR`), the
|
|
220
|
+
server-level value wins. A token that is another flag's value (the `-m` in
|
|
221
|
+
`-c -m`) or that comes after `--` does not count as setting a flag. The `codex`
|
|
222
|
+
tool leaves the matching per-call parameter (`cd`, `model`, `sandbox`,
|
|
223
|
+
`profile`, `ephemeral`, `skip_git_repo_check`) out of the command and names it
|
|
224
|
+
in the result together with the server-level value used instead: in the text
|
|
225
|
+
after the `[codex]` line (`cd="/other"; this server runs with -C /srv`) and in
|
|
226
|
+
`structuredContent.ignoredParameters`. Its schema tells the caller which
|
|
227
|
+
parameters the server already sets, and to what. `codex-reply` keeps such
|
|
228
|
+
parameters, because after `resume` they are the subcommand's own flags and
|
|
229
|
+
`codex` accepts them there. `extra_args` is passed through unchecked, so a flag
|
|
230
|
+
repeated through it still fails with `codex`'s own error.
|
|
215
231
|
|
|
216
232
|
## Tool: `codex`
|
|
217
233
|
|
|
@@ -226,7 +242,10 @@ The tool returns that final message followed by one metadata line:
|
|
|
226
242
|
```
|
|
227
243
|
|
|
228
244
|
`thread_id` and `status` are always present; the token fields appear when the
|
|
229
|
-
run reported them.
|
|
245
|
+
run reported them. A per-call parameter that a server-level flag overrode is
|
|
246
|
+
named after this line, with the value used instead (see
|
|
247
|
+
[Server-level flags](#server-level-flags)).
|
|
248
|
+
`isError` is the flag on an MCP tool result that tells the
|
|
230
249
|
client a call failed. This server sets it when `codex` exited non-zero, when the
|
|
231
250
|
subprocess timed out, and when the turn itself failed. The last case matters
|
|
232
251
|
because `codex exec` exits 0 after a failure inside the model API; the tool
|
|
@@ -136,7 +136,9 @@ After:
|
|
|
136
136
|
Parameter names differ from the old server where `codex exec` names the flag
|
|
137
137
|
differently: `cwd` is now `cd` (the `-C/--cd` flag), and `codex-reply` takes
|
|
138
138
|
`thread_id` instead of `threadId`. The result's `structuredContent` field keeps
|
|
139
|
-
the shape the old server returned, `{"threadId": ..., "content": ...}`.
|
|
139
|
+
the shape the old server returned, `{"threadId": ..., "content": ...}`. It
|
|
140
|
+
gains an `ignoredParameters` key only when the server's own flags overrode a
|
|
141
|
+
per-call parameter (see [Server-level flags](#server-level-flags)).
|
|
140
142
|
|
|
141
143
|
### Other MCP clients
|
|
142
144
|
|
|
@@ -172,11 +174,25 @@ in reasoning effort look like this:
|
|
|
172
174
|
|
|
173
175
|
A call to `codex-high` runs
|
|
174
176
|
`codex exec -c model_reasoning_effort=high [per-call flags] --json -`. Per-call
|
|
175
|
-
flags come after the server-level ones
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
flags come after the server-level ones. `-c` and `--add-dir` may repeat. For
|
|
178
|
+
`-c` the last one wins, so a per-call `config` entry overrides a server-level
|
|
179
|
+
`-c`.
|
|
180
|
+
|
|
181
|
+
The other typed flags (`-C`, `-m`, `--sandbox`, `-p`, `--ephemeral`,
|
|
182
|
+
`--skip-git-repo-check`) may appear only once: `codex` exits with "cannot be used
|
|
183
|
+
multiple times" on a second one. When the server's command line already sets one
|
|
184
|
+
of them, in any spelling (for example `-C DIR`, `-CDIR`, `--cd=DIR`), the
|
|
185
|
+
server-level value wins. A token that is another flag's value (the `-m` in
|
|
186
|
+
`-c -m`) or that comes after `--` does not count as setting a flag. The `codex`
|
|
187
|
+
tool leaves the matching per-call parameter (`cd`, `model`, `sandbox`,
|
|
188
|
+
`profile`, `ephemeral`, `skip_git_repo_check`) out of the command and names it
|
|
189
|
+
in the result together with the server-level value used instead: in the text
|
|
190
|
+
after the `[codex]` line (`cd="/other"; this server runs with -C /srv`) and in
|
|
191
|
+
`structuredContent.ignoredParameters`. Its schema tells the caller which
|
|
192
|
+
parameters the server already sets, and to what. `codex-reply` keeps such
|
|
193
|
+
parameters, because after `resume` they are the subcommand's own flags and
|
|
194
|
+
`codex` accepts them there. `extra_args` is passed through unchecked, so a flag
|
|
195
|
+
repeated through it still fails with `codex`'s own error.
|
|
180
196
|
|
|
181
197
|
## Tool: `codex`
|
|
182
198
|
|
|
@@ -191,7 +207,10 @@ The tool returns that final message followed by one metadata line:
|
|
|
191
207
|
```
|
|
192
208
|
|
|
193
209
|
`thread_id` and `status` are always present; the token fields appear when the
|
|
194
|
-
run reported them.
|
|
210
|
+
run reported them. A per-call parameter that a server-level flag overrode is
|
|
211
|
+
named after this line, with the value used instead (see
|
|
212
|
+
[Server-level flags](#server-level-flags)).
|
|
213
|
+
`isError` is the flag on an MCP tool result that tells the
|
|
195
214
|
client a call failed. This server sets it when `codex` exited non-zero, when the
|
|
196
215
|
subprocess timed out, and when the turn itself failed. The last case matters
|
|
197
216
|
because `codex exec` exits 0 after a failure inside the model API; the tool
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codex-cli-mcp-slim"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "A thin, auditable MCP server wrapping the Codex CLI (codex exec). Same tools as the deprecated `codex mcp-server`; forward-compatible with future CLI flags via extra_args passthrough."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -20,10 +20,11 @@ import contextlib
|
|
|
20
20
|
import json
|
|
21
21
|
import logging
|
|
22
22
|
import os
|
|
23
|
+
import shlex
|
|
23
24
|
import signal
|
|
24
25
|
import sys
|
|
25
26
|
from importlib.metadata import version as _pkg_version
|
|
26
|
-
from typing import Any
|
|
27
|
+
from typing import Any, NamedTuple
|
|
27
28
|
|
|
28
29
|
from mcp.server import Server, ServerRequestContext
|
|
29
30
|
from mcp.server.stdio import stdio_server
|
|
@@ -49,12 +50,89 @@ _PKG_NAME = "codex-cli-mcp-slim"
|
|
|
49
50
|
#
|
|
50
51
|
# uvx codex-cli-mcp-slim -c model_reasoning_effort=high -C /srv/scratch
|
|
51
52
|
#
|
|
52
|
-
# `-c` may repeat
|
|
53
|
-
# `-c`.
|
|
54
|
-
# one, and the tool result carries that error. Keep server-level flags and per-call
|
|
55
|
-
# parameters disjoint for those.
|
|
53
|
+
# `-c` and `--add-dir` may repeat (for `-c` the last one wins), so a per-call `config` can
|
|
54
|
+
# override a server-level `-c`. The flags in _SINGLE_USE_FLAGS may not: see there.
|
|
56
55
|
SERVER_ARGS: list[str] = []
|
|
57
56
|
|
|
57
|
+
# Per-call parameters whose flag codex accepts only once, with every spelling of that flag.
|
|
58
|
+
# A second occurrence makes `codex exec` exit 2 with "cannot be used multiple times", so when
|
|
59
|
+
# the server's own command line already carries one of these flags, the per-call parameter
|
|
60
|
+
# is left out of argv and the server-level value is used: whoever started the server fixed
|
|
61
|
+
# that configuration on purpose.
|
|
62
|
+
_SINGLE_USE_FLAGS: dict[str, tuple[str, ...]] = {
|
|
63
|
+
"cd": ("-C", "--cd"),
|
|
64
|
+
"model": ("-m", "--model"),
|
|
65
|
+
"sandbox": ("-s", "--sandbox"),
|
|
66
|
+
"profile": ("-p", "--profile"),
|
|
67
|
+
"ephemeral": ("--ephemeral",),
|
|
68
|
+
"skip_git_repo_check": ("--skip-git-repo-check",),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# The `codex exec` options (codex-cli 0.159.2) that take the next token as their value, so
|
|
73
|
+
# _params_set_by reads that token as a value and never as a flag of its own.
|
|
74
|
+
_VALUE_FLAGS = frozenset(
|
|
75
|
+
{
|
|
76
|
+
*("-c", "--config", "--enable", "--disable", "-i", "--image"),
|
|
77
|
+
*("-m", "--model", "--local-provider", "-p", "--profile", "-s", "--sandbox"),
|
|
78
|
+
*("-C", "--cd", "--add-dir", "--thread-source", "--output-schema", "--color"),
|
|
79
|
+
*("-o", "--output-last-message"),
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _ServerSetting(NamedTuple):
|
|
85
|
+
"""How the server's own command line sets one of the _SINGLE_USE_FLAGS parameters."""
|
|
86
|
+
|
|
87
|
+
value: str | bool # the flag's value, or True for a flag without one
|
|
88
|
+
written: str # the flag as that command line spells it, for example `-C /srv`
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _params_set_by(server_args: list[str]) -> dict[str, _ServerSetting]:
|
|
92
|
+
"""The _SINGLE_USE_FLAGS parameters whose flag already appears in `server_args`.
|
|
93
|
+
|
|
94
|
+
A flag counts in every form codex parses as that flag: `--cd DIR`, `--cd=DIR`, `-C DIR`
|
|
95
|
+
and the attached short forms `-CDIR` and `-C=DIR`. Short flags are case-sensitive, so the
|
|
96
|
+
repeatable `-c` is not mistaken for `-C`. The token after a _VALUE_FLAGS option is its
|
|
97
|
+
value (`-m` in `-c -m` is not the model flag), and nothing after `--` is a flag.
|
|
98
|
+
"""
|
|
99
|
+
held: dict[str, _ServerSetting] = {}
|
|
100
|
+
tokens = iter(server_args)
|
|
101
|
+
for token in tokens:
|
|
102
|
+
if token == "--":
|
|
103
|
+
break
|
|
104
|
+
if token in _VALUE_FLAGS:
|
|
105
|
+
value = next(tokens, None)
|
|
106
|
+
if value is None:
|
|
107
|
+
break
|
|
108
|
+
param = next((p for p, flags in _SINGLE_USE_FLAGS.items() if token in flags), None)
|
|
109
|
+
if param is not None:
|
|
110
|
+
held.setdefault(param, _ServerSetting(value, shlex.join([token, value])))
|
|
111
|
+
continue
|
|
112
|
+
for param, spellings in _SINGLE_USE_FLAGS.items():
|
|
113
|
+
for flag in spellings:
|
|
114
|
+
if token == flag:
|
|
115
|
+
held.setdefault(param, _ServerSetting(True, token))
|
|
116
|
+
elif len(flag) == 2 and token.startswith(flag):
|
|
117
|
+
value = token[2:].removeprefix("=")
|
|
118
|
+
held.setdefault(param, _ServerSetting(value, shlex.quote(token)))
|
|
119
|
+
elif token.startswith(flag + "="):
|
|
120
|
+
value = token[len(flag) + 1 :]
|
|
121
|
+
held.setdefault(param, _ServerSetting(value, shlex.quote(token)))
|
|
122
|
+
return held
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# What SERVER_ARGS sets among the _SINGLE_USE_FLAGS parameters. _set_server_args parses it
|
|
126
|
+
# once, together with SERVER_ARGS, so _build_argv, _invoke and on_list_tools read one answer.
|
|
127
|
+
_SERVER_SET: dict[str, _ServerSetting] = {}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _set_server_args(server_args: list[str]) -> None:
|
|
131
|
+
global SERVER_ARGS, _SERVER_SET
|
|
132
|
+
SERVER_ARGS = list(server_args)
|
|
133
|
+
_SERVER_SET = _params_set_by(SERVER_ARGS)
|
|
134
|
+
|
|
135
|
+
|
|
58
136
|
# The Server is built at the bottom of this file: v2 takes the handlers as constructor
|
|
59
137
|
# arguments, so they must already be defined by the time it is constructed.
|
|
60
138
|
|
|
@@ -73,8 +151,11 @@ def _build_argv(
|
|
|
73
151
|
extra_args: list[str] | None = None,
|
|
74
152
|
server_args: list[str] | None = None,
|
|
75
153
|
) -> list[str]:
|
|
76
|
-
|
|
77
|
-
|
|
154
|
+
if server_args is None:
|
|
155
|
+
server_args, server_set = SERVER_ARGS, _SERVER_SET
|
|
156
|
+
else:
|
|
157
|
+
server_set = _params_set_by(server_args)
|
|
158
|
+
argv: list[str] = [CODEX_CMD, "exec", *server_args]
|
|
78
159
|
if thread_id:
|
|
79
160
|
# `codex exec resume <id>` is a subcommand: it has to come before the per-call flags,
|
|
80
161
|
# and it accepts only a subset of them (the `codex-reply` schema advertises just that
|
|
@@ -82,21 +163,25 @@ def _build_argv(
|
|
|
82
163
|
# `exec` options there, so a server pinned to a working directory keeps that
|
|
83
164
|
# directory for replies as well.
|
|
84
165
|
argv += ["resume", thread_id]
|
|
85
|
-
|
|
166
|
+
# Per-call flags after `resume` are the subcommand's own, and codex accepts them there
|
|
167
|
+
# even when the same flag sits among the server-level ones, so only a plain `exec` call
|
|
168
|
+
# leaves out what the server already sets.
|
|
169
|
+
held = {} if thread_id else server_set
|
|
170
|
+
if cd and "cd" not in held:
|
|
86
171
|
argv += ["-C", cd]
|
|
87
|
-
if model:
|
|
172
|
+
if model and "model" not in held:
|
|
88
173
|
argv += ["-m", model]
|
|
89
174
|
for item in config or []:
|
|
90
175
|
argv += ["-c", item] # repeatable flag: one -c per key=value
|
|
91
|
-
if sandbox:
|
|
176
|
+
if sandbox and "sandbox" not in held:
|
|
92
177
|
argv += ["--sandbox", sandbox]
|
|
93
178
|
for d in add_dir or []:
|
|
94
179
|
argv += ["--add-dir", d] # repeatable flag: one --add-dir per directory
|
|
95
|
-
if profile:
|
|
180
|
+
if profile and "profile" not in held:
|
|
96
181
|
argv += ["-p", profile]
|
|
97
|
-
if ephemeral:
|
|
182
|
+
if ephemeral and "ephemeral" not in held:
|
|
98
183
|
argv.append("--ephemeral")
|
|
99
|
-
if skip_git_repo_check:
|
|
184
|
+
if skip_git_repo_check and "skip_git_repo_check" not in held:
|
|
100
185
|
argv.append("--skip-git-repo-check")
|
|
101
186
|
if extra_args:
|
|
102
187
|
argv += list(extra_args)
|
|
@@ -608,11 +693,33 @@ async def _invoke(arguments: dict[str, Any] | None, *, reply: bool) -> CallToolR
|
|
|
608
693
|
)
|
|
609
694
|
parsed = _parse_events(result.get("stdout", ""))
|
|
610
695
|
text = _format_result(result)
|
|
696
|
+
# Name what _build_argv left out, and the value used instead, so a caller does not take a
|
|
697
|
+
# run in the server's directory (say) for a run in the one it asked for.
|
|
698
|
+
held = {} if reply else _SERVER_SET
|
|
699
|
+
dropped = [param for param in _SINGLE_USE_FLAGS if param in held and args.get(param)]
|
|
611
700
|
structured: dict[str, Any] | None = None
|
|
612
701
|
if parsed["thread_id"]:
|
|
613
702
|
# The same shape the deprecated `codex mcp-server` returned, so a client that reads
|
|
614
703
|
# structuredContent.threadId keeps working.
|
|
615
704
|
structured = {"threadId": parsed["thread_id"], "content": _response_text(parsed)}
|
|
705
|
+
if dropped:
|
|
706
|
+
lines = [
|
|
707
|
+
f"{p}={json.dumps(args[p], ensure_ascii=False)}; "
|
|
708
|
+
f"this server runs with {held[p].written}"
|
|
709
|
+
for p in dropped
|
|
710
|
+
]
|
|
711
|
+
logger.warning("per-call parameters ignored: %s", " | ".join(lines))
|
|
712
|
+
text += (
|
|
713
|
+
"\n\nignored, because this server's own command line already sets the flag "
|
|
714
|
+
"and codex takes it only once:\n" + "\n".join(lines)
|
|
715
|
+
)
|
|
716
|
+
# A client that reads only structuredContent learns it here.
|
|
717
|
+
structured = {
|
|
718
|
+
**(structured or {}),
|
|
719
|
+
"ignoredParameters": [
|
|
720
|
+
{"name": p, "value": args[p], "serverValue": held[p].value} for p in dropped
|
|
721
|
+
],
|
|
722
|
+
}
|
|
616
723
|
return CallToolResult(
|
|
617
724
|
content=[TextContent(type="text", text=text)],
|
|
618
725
|
structured_content=structured,
|
|
@@ -631,7 +738,23 @@ async def codex_reply(arguments: dict[str, Any] | None) -> CallToolResult:
|
|
|
631
738
|
async def on_list_tools(
|
|
632
739
|
ctx: ServerRequestContext, params: PaginatedRequestParams | None
|
|
633
740
|
) -> ListToolsResult:
|
|
634
|
-
|
|
741
|
+
# The caller cannot see the server's command line, so the `codex` schema says which
|
|
742
|
+
# parameters it already fixes, rather than inviting values that _build_argv would drop.
|
|
743
|
+
if not _SERVER_SET:
|
|
744
|
+
return ListToolsResult(tools=[_CODEX_TOOL, _REPLY_TOOL])
|
|
745
|
+
props = dict(_CODEX_PROPS)
|
|
746
|
+
for param, setting in _SERVER_SET.items():
|
|
747
|
+
props[param] = {
|
|
748
|
+
**props[param],
|
|
749
|
+
"description": (
|
|
750
|
+
f"{props[param]['description']} This server's own command line already "
|
|
751
|
+
f"sets {setting.written}, so a value passed here is ignored."
|
|
752
|
+
),
|
|
753
|
+
}
|
|
754
|
+
codex_tool = _CODEX_TOOL.model_copy(
|
|
755
|
+
update={"input_schema": {**_CODEX_TOOL.input_schema, "properties": props}}
|
|
756
|
+
)
|
|
757
|
+
return ListToolsResult(tools=[codex_tool, _REPLY_TOOL])
|
|
635
758
|
|
|
636
759
|
|
|
637
760
|
async def on_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
|
|
@@ -691,7 +814,7 @@ def main() -> None:
|
|
|
691
814
|
if sys.argv[1:] == ["--version"]:
|
|
692
815
|
print(f"{_PKG_NAME} {_pkg_version(_PKG_NAME)}")
|
|
693
816
|
return
|
|
694
|
-
|
|
817
|
+
_set_server_args(sys.argv[1:])
|
|
695
818
|
asyncio.run(_amain())
|
|
696
819
|
|
|
697
820
|
|
{codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1/src/codex_cli_mcp_slim.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-cli-mcp-slim
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: A thin, auditable MCP server wrapping the Codex CLI (codex exec). Same tools as the deprecated `codex mcp-server`; forward-compatible with future CLI flags via extra_args passthrough.
|
|
5
5
|
Author-email: tksfjt1024 <tksfjt1024@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -171,7 +171,9 @@ After:
|
|
|
171
171
|
Parameter names differ from the old server where `codex exec` names the flag
|
|
172
172
|
differently: `cwd` is now `cd` (the `-C/--cd` flag), and `codex-reply` takes
|
|
173
173
|
`thread_id` instead of `threadId`. The result's `structuredContent` field keeps
|
|
174
|
-
the shape the old server returned, `{"threadId": ..., "content": ...}`.
|
|
174
|
+
the shape the old server returned, `{"threadId": ..., "content": ...}`. It
|
|
175
|
+
gains an `ignoredParameters` key only when the server's own flags overrode a
|
|
176
|
+
per-call parameter (see [Server-level flags](#server-level-flags)).
|
|
175
177
|
|
|
176
178
|
### Other MCP clients
|
|
177
179
|
|
|
@@ -207,11 +209,25 @@ in reasoning effort look like this:
|
|
|
207
209
|
|
|
208
210
|
A call to `codex-high` runs
|
|
209
211
|
`codex exec -c model_reasoning_effort=high [per-call flags] --json -`. Per-call
|
|
210
|
-
flags come after the server-level ones
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
flags come after the server-level ones. `-c` and `--add-dir` may repeat. For
|
|
213
|
+
`-c` the last one wins, so a per-call `config` entry overrides a server-level
|
|
214
|
+
`-c`.
|
|
215
|
+
|
|
216
|
+
The other typed flags (`-C`, `-m`, `--sandbox`, `-p`, `--ephemeral`,
|
|
217
|
+
`--skip-git-repo-check`) may appear only once: `codex` exits with "cannot be used
|
|
218
|
+
multiple times" on a second one. When the server's command line already sets one
|
|
219
|
+
of them, in any spelling (for example `-C DIR`, `-CDIR`, `--cd=DIR`), the
|
|
220
|
+
server-level value wins. A token that is another flag's value (the `-m` in
|
|
221
|
+
`-c -m`) or that comes after `--` does not count as setting a flag. The `codex`
|
|
222
|
+
tool leaves the matching per-call parameter (`cd`, `model`, `sandbox`,
|
|
223
|
+
`profile`, `ephemeral`, `skip_git_repo_check`) out of the command and names it
|
|
224
|
+
in the result together with the server-level value used instead: in the text
|
|
225
|
+
after the `[codex]` line (`cd="/other"; this server runs with -C /srv`) and in
|
|
226
|
+
`structuredContent.ignoredParameters`. Its schema tells the caller which
|
|
227
|
+
parameters the server already sets, and to what. `codex-reply` keeps such
|
|
228
|
+
parameters, because after `resume` they are the subcommand's own flags and
|
|
229
|
+
`codex` accepts them there. `extra_args` is passed through unchecked, so a flag
|
|
230
|
+
repeated through it still fails with `codex`'s own error.
|
|
215
231
|
|
|
216
232
|
## Tool: `codex`
|
|
217
233
|
|
|
@@ -226,7 +242,10 @@ The tool returns that final message followed by one metadata line:
|
|
|
226
242
|
```
|
|
227
243
|
|
|
228
244
|
`thread_id` and `status` are always present; the token fields appear when the
|
|
229
|
-
run reported them.
|
|
245
|
+
run reported them. A per-call parameter that a server-level flag overrode is
|
|
246
|
+
named after this line, with the value used instead (see
|
|
247
|
+
[Server-level flags](#server-level-flags)).
|
|
248
|
+
`isError` is the flag on an MCP tool result that tells the
|
|
230
249
|
client a call failed. This server sets it when `codex` exited non-zero, when the
|
|
231
250
|
subprocess timed out, and when the turn itself failed. The last case matters
|
|
232
251
|
because `codex exec` exits 0 after a failure inside the model API; the tool
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Tests for argv construction, including how per-call flags meet server-level ones.
|
|
2
|
+
|
|
3
|
+
The subprocess is never started here; the tests that go through the tool replace
|
|
4
|
+
`_run_codex` with a stub that records the argv it was handed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from codex_cli_mcp_slim import server
|
|
10
|
+
from codex_cli_mcp_slim.server import CODEX_CMD, _build_argv
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _ok_run(**kwargs):
|
|
14
|
+
stdout = (
|
|
15
|
+
'{"type": "thread.started", "thread_id": "t"}\n'
|
|
16
|
+
'{"type": "item.completed", "item": {"type": "agent_message", "text": "answer"}}\n'
|
|
17
|
+
'{"type": "turn.completed", "usage": {}}\n'
|
|
18
|
+
)
|
|
19
|
+
return {"ok": True, "returncode": 0, "stdout": stdout, "stderr": "", "argv": kwargs["argv"]}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def start_server_with(monkeypatch):
|
|
24
|
+
"""Set the server's own command line the way main() does, restored after the test."""
|
|
25
|
+
monkeypatch.setattr(server, "SERVER_ARGS", server.SERVER_ARGS)
|
|
26
|
+
monkeypatch.setattr(server, "_SERVER_SET", server._SERVER_SET)
|
|
27
|
+
return server._set_server_args
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_build_argv_minimal():
|
|
31
|
+
argv = _build_argv(server_args=[])
|
|
32
|
+
assert argv == [CODEX_CMD, "exec", "--json", "-"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_build_argv_ends_with_json_and_stdin_prompt():
|
|
36
|
+
# --json and `-` stay last so nothing in extra_args can capture `-` as its own value,
|
|
37
|
+
# and so the prompt is read from stdin rather than taken from argv.
|
|
38
|
+
argv = _build_argv(server_args=[], extra_args=["--output-schema", "/tmp/schema.json"])
|
|
39
|
+
assert argv[-2:] == ["--json", "-"]
|
|
40
|
+
assert argv[-4:-2] == ["--output-schema", "/tmp/schema.json"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_build_argv_server_args_come_right_after_exec():
|
|
44
|
+
server_args = ["-c", "model_reasoning_effort=high", "-C", "/srv/scratch"]
|
|
45
|
+
argv = _build_argv(server_args=server_args)
|
|
46
|
+
assert argv[:6] == [CODEX_CMD, "exec", *server_args]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_build_argv_per_call_config_follows_server_args_so_it_wins():
|
|
50
|
+
# codex applies repeated -c flags in order, last one wins; a per-call override therefore
|
|
51
|
+
# has to come after the server-level one.
|
|
52
|
+
argv = _build_argv(
|
|
53
|
+
server_args=["-c", "model_reasoning_effort=high"],
|
|
54
|
+
config=["model_reasoning_effort=low"],
|
|
55
|
+
)
|
|
56
|
+
positions = [i for i, tok in enumerate(argv) if tok == "-c"]
|
|
57
|
+
assert argv[positions[0] + 1] == "model_reasoning_effort=high"
|
|
58
|
+
assert argv[positions[1] + 1] == "model_reasoning_effort=low"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_build_argv_resume_is_a_subcommand_before_per_call_flags():
|
|
62
|
+
argv = _build_argv(server_args=["-c", "k=v"], thread_id="thread-1", model="m")
|
|
63
|
+
assert argv[:6] == [CODEX_CMD, "exec", "-c", "k=v", "resume", "thread-1"]
|
|
64
|
+
assert argv[6:8] == ["-m", "m"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_build_argv_cd_maps_to_dash_c_upper():
|
|
68
|
+
argv = _build_argv(server_args=[], cd="/repo")
|
|
69
|
+
assert argv[argv.index("-C") + 1] == "/repo"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_build_argv_model_maps_to_dash_m():
|
|
73
|
+
argv = _build_argv(server_args=[], model="gpt-example")
|
|
74
|
+
assert argv[argv.index("-m") + 1] == "gpt-example"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_build_argv_config_is_repeated_not_comma_joined():
|
|
78
|
+
argv = _build_argv(server_args=[], config=["a=1", "b=2", "c=3"])
|
|
79
|
+
assert argv.count("-c") == 3
|
|
80
|
+
for item in ("a=1", "b=2", "c=3"):
|
|
81
|
+
assert argv[argv.index(item) - 1] == "-c"
|
|
82
|
+
assert "a=1,b=2,c=3" not in argv
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_build_argv_add_dir_is_repeated_not_comma_joined():
|
|
86
|
+
argv = _build_argv(server_args=[], add_dir=["/a", "/b", "/c"])
|
|
87
|
+
assert argv.count("--add-dir") == 3
|
|
88
|
+
for d in ("/a", "/b", "/c"):
|
|
89
|
+
assert argv[argv.index(d) - 1] == "--add-dir"
|
|
90
|
+
assert "/a,/b,/c" not in argv
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_build_argv_sandbox():
|
|
94
|
+
argv = _build_argv(server_args=[], sandbox="read-only")
|
|
95
|
+
assert argv[argv.index("--sandbox") + 1] == "read-only"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_build_argv_profile():
|
|
99
|
+
argv = _build_argv(server_args=[], profile="research")
|
|
100
|
+
assert argv[argv.index("-p") + 1] == "research"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_build_argv_boolean_flags():
|
|
104
|
+
argv = _build_argv(server_args=[], ephemeral=True, skip_git_repo_check=True)
|
|
105
|
+
assert "--ephemeral" in argv
|
|
106
|
+
assert "--skip-git-repo-check" in argv
|
|
107
|
+
argv = _build_argv(server_args=[])
|
|
108
|
+
assert "--ephemeral" not in argv
|
|
109
|
+
assert "--skip-git-repo-check" not in argv
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_build_argv_extra_args_appended_verbatim_before_json():
|
|
113
|
+
argv = _build_argv(server_args=[], extra_args=["--ignore-rules", "--enable", "x"])
|
|
114
|
+
assert argv[-5:-2] == ["--ignore-rules", "--enable", "x"]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_build_argv_all_typed_flags_combine():
|
|
118
|
+
argv = _build_argv(
|
|
119
|
+
server_args=[],
|
|
120
|
+
cd="/repo",
|
|
121
|
+
model="m",
|
|
122
|
+
config=["k=v"],
|
|
123
|
+
sandbox="workspace-write",
|
|
124
|
+
add_dir=["/x"],
|
|
125
|
+
profile="p",
|
|
126
|
+
ephemeral=True,
|
|
127
|
+
skip_git_repo_check=True,
|
|
128
|
+
extra_args=["--custom"],
|
|
129
|
+
)
|
|
130
|
+
for token in (
|
|
131
|
+
"-C",
|
|
132
|
+
"/repo",
|
|
133
|
+
"-m",
|
|
134
|
+
"m",
|
|
135
|
+
"-c",
|
|
136
|
+
"k=v",
|
|
137
|
+
"--sandbox",
|
|
138
|
+
"workspace-write",
|
|
139
|
+
"--add-dir",
|
|
140
|
+
"/x",
|
|
141
|
+
"-p",
|
|
142
|
+
"p",
|
|
143
|
+
"--ephemeral",
|
|
144
|
+
"--skip-git-repo-check",
|
|
145
|
+
"--custom",
|
|
146
|
+
):
|
|
147
|
+
assert token in argv
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# codex rejects a second -C, -m, -s, -p, --ephemeral or --skip-git-repo-check with "cannot
|
|
151
|
+
# be used multiple times" (exit 2), so a per-call parameter whose flag the server's own
|
|
152
|
+
# command line already sets is left out and the server-level value is used.
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _value_after(argv, *flags):
|
|
156
|
+
return [argv[i + 1] for i, tok in enumerate(argv) if tok in flags]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_build_argv_drops_per_call_flags_the_server_already_sets():
|
|
160
|
+
argv = _build_argv(
|
|
161
|
+
server_args=["-C", "/srv/scratch", "--skip-git-repo-check"],
|
|
162
|
+
cd="/other",
|
|
163
|
+
skip_git_repo_check=True,
|
|
164
|
+
)
|
|
165
|
+
assert argv.count("--skip-git-repo-check") == 1
|
|
166
|
+
assert _value_after(argv, "-C", "--cd") == ["/srv/scratch"]
|
|
167
|
+
assert "/other" not in argv
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@pytest.mark.parametrize(
|
|
171
|
+
("server_args", "param", "value"),
|
|
172
|
+
[
|
|
173
|
+
(["--cd", "/srv"], "cd", "/other"),
|
|
174
|
+
(["--cd=/srv"], "cd", "/other"),
|
|
175
|
+
(["-C/srv"], "cd", "/other"),
|
|
176
|
+
(["-C=/srv"], "cd", "/other"),
|
|
177
|
+
(["-m", "a"], "model", "b"),
|
|
178
|
+
(["--model=a"], "model", "b"),
|
|
179
|
+
(["-ma"], "model", "b"),
|
|
180
|
+
(["-s", "read-only"], "sandbox", "workspace-write"),
|
|
181
|
+
(["--sandbox=read-only"], "sandbox", "workspace-write"),
|
|
182
|
+
(["-p", "a"], "profile", "b"),
|
|
183
|
+
(["--profile", "a"], "profile", "b"),
|
|
184
|
+
(["--ephemeral"], "ephemeral", True),
|
|
185
|
+
(["--skip-git-repo-check"], "skip_git_repo_check", True),
|
|
186
|
+
],
|
|
187
|
+
)
|
|
188
|
+
def test_build_argv_recognises_every_spelling_of_a_server_level_flag(server_args, param, value):
|
|
189
|
+
argv = _build_argv(server_args=server_args, **{param: value})
|
|
190
|
+
assert argv == [CODEX_CMD, "exec", *server_args, "--json", "-"]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@pytest.mark.parametrize(
|
|
194
|
+
"server_args",
|
|
195
|
+
[
|
|
196
|
+
["-c", "-model=1"], # the value of -c, not the model flag
|
|
197
|
+
["--", "-m"], # past the end of the flags
|
|
198
|
+
],
|
|
199
|
+
)
|
|
200
|
+
def test_build_argv_reads_neither_a_value_nor_a_token_after_double_dash_as_a_flag(server_args):
|
|
201
|
+
assert "model" not in server._params_set_by(server_args)
|
|
202
|
+
argv = _build_argv(server_args=server_args, model="b")
|
|
203
|
+
assert argv == [CODEX_CMD, "exec", *server_args, "-m", "b", "--json", "-"]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@pytest.mark.parametrize(
|
|
207
|
+
("server_args", "value", "written"),
|
|
208
|
+
[
|
|
209
|
+
(["-C", "/srv"], "/srv", "-C /srv"),
|
|
210
|
+
(["-c", "k=v", "--cd", "/srv"], "/srv", "--cd /srv"),
|
|
211
|
+
(["--cd=/srv"], "/srv", "--cd=/srv"),
|
|
212
|
+
(["-C/srv"], "/srv", "-C/srv"),
|
|
213
|
+
(["-C=/srv"], "/srv", "-C=/srv"),
|
|
214
|
+
(["-C", "/my dir"], "/my dir", "-C '/my dir'"),
|
|
215
|
+
],
|
|
216
|
+
)
|
|
217
|
+
def test_params_set_by_keeps_the_server_level_value_and_its_spelling(server_args, value, written):
|
|
218
|
+
assert server._params_set_by(server_args) == {"cd": (value, written)}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_params_set_by_reports_a_flag_without_a_value_as_true():
|
|
222
|
+
assert server._params_set_by(["--ephemeral"]) == {"ephemeral": (True, "--ephemeral")}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_build_argv_keeps_per_call_flags_the_server_does_not_set():
|
|
226
|
+
argv = _build_argv(
|
|
227
|
+
server_args=["--skip-git-repo-check"],
|
|
228
|
+
cd="/repo",
|
|
229
|
+
model="m",
|
|
230
|
+
sandbox="read-only",
|
|
231
|
+
profile="p",
|
|
232
|
+
ephemeral=True,
|
|
233
|
+
)
|
|
234
|
+
assert _value_after(argv, "-C") == ["/repo"]
|
|
235
|
+
assert _value_after(argv, "-m") == ["m"]
|
|
236
|
+
assert _value_after(argv, "--sandbox") == ["read-only"]
|
|
237
|
+
assert _value_after(argv, "-p") == ["p"]
|
|
238
|
+
assert "--ephemeral" in argv
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_build_argv_server_level_dash_c_lower_is_not_mistaken_for_cd():
|
|
242
|
+
argv = _build_argv(server_args=["-c", "model_reasoning_effort=high"], cd="/repo")
|
|
243
|
+
assert _value_after(argv, "-C") == ["/repo"]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_build_argv_repeatable_flags_still_stack_on_server_level_ones():
|
|
247
|
+
argv = _build_argv(
|
|
248
|
+
server_args=["-c", "k=1", "--add-dir", "/a"],
|
|
249
|
+
config=["k=2"],
|
|
250
|
+
add_dir=["/b"],
|
|
251
|
+
)
|
|
252
|
+
assert _value_after(argv, "-c") == ["k=1", "k=2"]
|
|
253
|
+
assert _value_after(argv, "--add-dir") == ["/a", "/b"]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_build_argv_reply_keeps_per_call_flags_after_resume():
|
|
257
|
+
# After `resume` the per-call flags belong to the subcommand, which codex parses on its
|
|
258
|
+
# own, so a repeat of a server-level flag is accepted there and nothing is dropped.
|
|
259
|
+
argv = _build_argv(
|
|
260
|
+
server_args=["--skip-git-repo-check", "-m", "a"],
|
|
261
|
+
thread_id="t",
|
|
262
|
+
skip_git_repo_check=True,
|
|
263
|
+
model="b",
|
|
264
|
+
)
|
|
265
|
+
after_resume = argv[argv.index("resume") + 2 :]
|
|
266
|
+
assert "--skip-git-repo-check" in after_resume
|
|
267
|
+
assert _value_after(after_resume, "-m") == ["b"]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
async def test_tool_reports_the_per_call_parameters_it_dropped(monkeypatch, start_server_with):
|
|
271
|
+
seen = {}
|
|
272
|
+
|
|
273
|
+
async def fake_run(**kwargs):
|
|
274
|
+
seen.update(kwargs)
|
|
275
|
+
return _ok_run(**kwargs)
|
|
276
|
+
|
|
277
|
+
start_server_with(["-C", "/srv", "--skip-git-repo-check"])
|
|
278
|
+
monkeypatch.setattr(server, "_run_codex", fake_run)
|
|
279
|
+
result = await server.codex(
|
|
280
|
+
{"prompt": "p", "cd": "/other", "skip_git_repo_check": True, "model": "m"}
|
|
281
|
+
)
|
|
282
|
+
assert seen["argv"].count("--skip-git-repo-check") == 1
|
|
283
|
+
assert _value_after(seen["argv"], "-C") == ["/srv"]
|
|
284
|
+
assert _value_after(seen["argv"], "-m") == ["m"]
|
|
285
|
+
text = result.content[0].text
|
|
286
|
+
assert result.is_error is False
|
|
287
|
+
assert text.startswith("answer\n\n[codex] thread_id=t")
|
|
288
|
+
assert 'cd="/other"; this server runs with -C /srv' in text
|
|
289
|
+
assert "skip_git_repo_check=true; this server runs with --skip-git-repo-check" in text
|
|
290
|
+
assert "model=" not in text
|
|
291
|
+
assert result.structured_content == {
|
|
292
|
+
"threadId": "t",
|
|
293
|
+
"content": "answer",
|
|
294
|
+
"ignoredParameters": [
|
|
295
|
+
{"name": "cd", "value": "/other", "serverValue": "/srv"},
|
|
296
|
+
{"name": "skip_git_repo_check", "value": True, "serverValue": True},
|
|
297
|
+
],
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
async def test_tool_reports_dropped_parameters_in_structured_content_without_a_thread(
|
|
302
|
+
monkeypatch, start_server_with
|
|
303
|
+
):
|
|
304
|
+
async def fake_run(**kwargs):
|
|
305
|
+
return {"ok": False, "error": "boom", "argv": kwargs["argv"]}
|
|
306
|
+
|
|
307
|
+
start_server_with(["--model=a"])
|
|
308
|
+
monkeypatch.setattr(server, "_run_codex", fake_run)
|
|
309
|
+
result = await server.codex({"prompt": "p", "model": "b"})
|
|
310
|
+
assert result.is_error is True
|
|
311
|
+
assert result.content[0].text.startswith("[ERROR] boom")
|
|
312
|
+
assert 'model="b"; this server runs with --model=a' in result.content[0].text
|
|
313
|
+
assert result.structured_content == {
|
|
314
|
+
"ignoredParameters": [{"name": "model", "value": "b", "serverValue": "a"}]
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
async def test_tool_adds_nothing_when_no_per_call_parameter_was_dropped(
|
|
319
|
+
monkeypatch, start_server_with
|
|
320
|
+
):
|
|
321
|
+
async def fake_run(**kwargs):
|
|
322
|
+
return _ok_run(**kwargs)
|
|
323
|
+
|
|
324
|
+
start_server_with(["--skip-git-repo-check"])
|
|
325
|
+
monkeypatch.setattr(server, "_run_codex", fake_run)
|
|
326
|
+
result = await server.codex({"prompt": "p", "cd": "/repo"})
|
|
327
|
+
assert result.content[0].text == "answer\n\n[codex] thread_id=t status=completed"
|
|
328
|
+
assert result.structured_content == {"threadId": "t", "content": "answer"}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
async def test_tool_schema_names_the_parameters_the_server_already_sets(start_server_with):
|
|
332
|
+
def props():
|
|
333
|
+
return {tool.name: tool.input_schema["properties"] for tool in listed.tools}
|
|
334
|
+
|
|
335
|
+
start_server_with(["--skip-git-repo-check", "-C", "/srv"])
|
|
336
|
+
listed = await server.on_list_tools(None, None)
|
|
337
|
+
assert (
|
|
338
|
+
"already sets --skip-git-repo-check"
|
|
339
|
+
in props()["codex"]["skip_git_repo_check"]["description"]
|
|
340
|
+
)
|
|
341
|
+
assert "already sets -C /srv," in props()["codex"]["cd"]["description"]
|
|
342
|
+
assert "already" not in props()["codex"]["model"]["description"]
|
|
343
|
+
|
|
344
|
+
start_server_with([])
|
|
345
|
+
listed = await server.on_list_tools(None, None)
|
|
346
|
+
assert "already" not in props()["codex"]["skip_git_repo_check"]["description"]
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
"""Pure-function tests for argv construction. Subprocess execution is not tested here."""
|
|
2
|
-
|
|
3
|
-
from codex_cli_mcp_slim.server import CODEX_CMD, _build_argv
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_build_argv_minimal():
|
|
7
|
-
argv = _build_argv(server_args=[])
|
|
8
|
-
assert argv == [CODEX_CMD, "exec", "--json", "-"]
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_build_argv_ends_with_json_and_stdin_prompt():
|
|
12
|
-
# --json and `-` stay last so nothing in extra_args can capture `-` as its own value,
|
|
13
|
-
# and so the prompt is read from stdin rather than taken from argv.
|
|
14
|
-
argv = _build_argv(server_args=[], extra_args=["--output-schema", "/tmp/schema.json"])
|
|
15
|
-
assert argv[-2:] == ["--json", "-"]
|
|
16
|
-
assert argv[-4:-2] == ["--output-schema", "/tmp/schema.json"]
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_build_argv_server_args_come_right_after_exec():
|
|
20
|
-
server_args = ["-c", "model_reasoning_effort=high", "-C", "/srv/scratch"]
|
|
21
|
-
argv = _build_argv(server_args=server_args)
|
|
22
|
-
assert argv[:6] == [CODEX_CMD, "exec", *server_args]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_build_argv_per_call_config_follows_server_args_so_it_wins():
|
|
26
|
-
# codex applies repeated -c flags in order, last one wins; a per-call override therefore
|
|
27
|
-
# has to come after the server-level one.
|
|
28
|
-
argv = _build_argv(
|
|
29
|
-
server_args=["-c", "model_reasoning_effort=high"],
|
|
30
|
-
config=["model_reasoning_effort=low"],
|
|
31
|
-
)
|
|
32
|
-
positions = [i for i, tok in enumerate(argv) if tok == "-c"]
|
|
33
|
-
assert argv[positions[0] + 1] == "model_reasoning_effort=high"
|
|
34
|
-
assert argv[positions[1] + 1] == "model_reasoning_effort=low"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_build_argv_resume_is_a_subcommand_before_per_call_flags():
|
|
38
|
-
argv = _build_argv(server_args=["-c", "k=v"], thread_id="thread-1", model="m")
|
|
39
|
-
assert argv[:6] == [CODEX_CMD, "exec", "-c", "k=v", "resume", "thread-1"]
|
|
40
|
-
assert argv[6:8] == ["-m", "m"]
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_build_argv_cd_maps_to_dash_c_upper():
|
|
44
|
-
argv = _build_argv(server_args=[], cd="/repo")
|
|
45
|
-
assert argv[argv.index("-C") + 1] == "/repo"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def test_build_argv_model_maps_to_dash_m():
|
|
49
|
-
argv = _build_argv(server_args=[], model="gpt-example")
|
|
50
|
-
assert argv[argv.index("-m") + 1] == "gpt-example"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def test_build_argv_config_is_repeated_not_comma_joined():
|
|
54
|
-
argv = _build_argv(server_args=[], config=["a=1", "b=2", "c=3"])
|
|
55
|
-
assert argv.count("-c") == 3
|
|
56
|
-
for item in ("a=1", "b=2", "c=3"):
|
|
57
|
-
assert argv[argv.index(item) - 1] == "-c"
|
|
58
|
-
assert "a=1,b=2,c=3" not in argv
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_build_argv_add_dir_is_repeated_not_comma_joined():
|
|
62
|
-
argv = _build_argv(server_args=[], add_dir=["/a", "/b", "/c"])
|
|
63
|
-
assert argv.count("--add-dir") == 3
|
|
64
|
-
for d in ("/a", "/b", "/c"):
|
|
65
|
-
assert argv[argv.index(d) - 1] == "--add-dir"
|
|
66
|
-
assert "/a,/b,/c" not in argv
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def test_build_argv_sandbox():
|
|
70
|
-
argv = _build_argv(server_args=[], sandbox="read-only")
|
|
71
|
-
assert argv[argv.index("--sandbox") + 1] == "read-only"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_build_argv_profile():
|
|
75
|
-
argv = _build_argv(server_args=[], profile="research")
|
|
76
|
-
assert argv[argv.index("-p") + 1] == "research"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def test_build_argv_boolean_flags():
|
|
80
|
-
argv = _build_argv(server_args=[], ephemeral=True, skip_git_repo_check=True)
|
|
81
|
-
assert "--ephemeral" in argv
|
|
82
|
-
assert "--skip-git-repo-check" in argv
|
|
83
|
-
argv = _build_argv(server_args=[])
|
|
84
|
-
assert "--ephemeral" not in argv
|
|
85
|
-
assert "--skip-git-repo-check" not in argv
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def test_build_argv_extra_args_appended_verbatim_before_json():
|
|
89
|
-
argv = _build_argv(server_args=[], extra_args=["--ignore-rules", "--enable", "x"])
|
|
90
|
-
assert argv[-5:-2] == ["--ignore-rules", "--enable", "x"]
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def test_build_argv_all_typed_flags_combine():
|
|
94
|
-
argv = _build_argv(
|
|
95
|
-
server_args=[],
|
|
96
|
-
cd="/repo",
|
|
97
|
-
model="m",
|
|
98
|
-
config=["k=v"],
|
|
99
|
-
sandbox="workspace-write",
|
|
100
|
-
add_dir=["/x"],
|
|
101
|
-
profile="p",
|
|
102
|
-
ephemeral=True,
|
|
103
|
-
skip_git_repo_check=True,
|
|
104
|
-
extra_args=["--custom"],
|
|
105
|
-
)
|
|
106
|
-
for token in (
|
|
107
|
-
"-C",
|
|
108
|
-
"/repo",
|
|
109
|
-
"-m",
|
|
110
|
-
"m",
|
|
111
|
-
"-c",
|
|
112
|
-
"k=v",
|
|
113
|
-
"--sandbox",
|
|
114
|
-
"workspace-write",
|
|
115
|
-
"--add-dir",
|
|
116
|
-
"/x",
|
|
117
|
-
"-p",
|
|
118
|
-
"p",
|
|
119
|
-
"--ephemeral",
|
|
120
|
-
"--skip-git-repo-check",
|
|
121
|
-
"--custom",
|
|
122
|
-
):
|
|
123
|
-
assert token in argv
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/src/codex_cli_mcp_slim.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/src/codex_cli_mcp_slim.egg-info/requires.txt
RENAMED
|
File without changes
|
{codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/src/codex_cli_mcp_slim.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
{codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.1}/tests/test_subprocess_stdin_isolation.py
RENAMED
|
File without changes
|
|
File without changes
|