codex-cli-mcp-slim 0.1.0__tar.gz → 0.1.2__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.2}/PKG-INFO +52 -8
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.2}/README.md +51 -7
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.2}/pyproject.toml +1 -1
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.2}/src/codex_cli_mcp_slim/server.py +218 -27
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.2/src/codex_cli_mcp_slim.egg-info}/PKG-INFO +52 -8
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.2}/src/codex_cli_mcp_slim.egg-info/SOURCES.txt +1 -0
- codex_cli_mcp_slim-0.1.2/tests/test_basic.py +346 -0
- codex_cli_mcp_slim-0.1.2/tests/test_progress.py +306 -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.2}/LICENSE +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.2}/setup.cfg +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.2}/src/codex_cli_mcp_slim/__init__.py +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.2}/src/codex_cli_mcp_slim/py.typed +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.2}/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.2}/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.2}/src/codex_cli_mcp_slim.egg-info/requires.txt +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.2}/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.2}/tests/test_call_tool.py +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.2}/tests/test_subprocess_stdin_isolation.py +0 -0
- {codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.2}/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.2}/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.2
|
|
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
|
|
@@ -309,6 +328,31 @@ buffered output and reap the process, so the effective ceiling is
|
|
|
309
328
|
`timeout_seconds + 20`. A timed-out call is flagged `isError` and carries
|
|
310
329
|
whatever `codex` had printed so far.
|
|
311
330
|
|
|
331
|
+
An MCP client may give up on a call that has sent neither a response nor a
|
|
332
|
+
progress notification for a while. Claude Code 2.1.318 does so after 1800
|
|
333
|
+
seconds by default for a stdio server ("sent no response or progress for
|
|
334
|
+
1800s"), and a long `codex` turn can take longer than that. While `codex` runs,
|
|
335
|
+
the server therefore sends a `notifications/progress` message every 60 seconds,
|
|
336
|
+
so the call lasts until `codex` finishes or `timeout_seconds` runs out. A client
|
|
337
|
+
gets these only when it sent a `progressToken` with the call, as Claude Code
|
|
338
|
+
does; a client that gives up after less than 60 seconds without progress is not
|
|
339
|
+
covered. With the default `timeout_seconds` of 1800 the call still ends after
|
|
340
|
+
about 30 minutes, at the server's own timeout, so to let `codex` run longer,
|
|
341
|
+
raise `timeout_seconds` (30 to 3600 per call) or `$CODEX_CLI_MCP_SLIM_TIMEOUT`,
|
|
342
|
+
which sets the default. In the notification's `params`, `progress` counts the
|
|
343
|
+
notifications sent so far and `message` says how long `codex` has been running:
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
{
|
|
347
|
+
"jsonrpc": "2.0",
|
|
348
|
+
"method": "notifications/progress",
|
|
349
|
+
"params": {"progressToken": 7, "progress": 2, "message": "codex running for 120s"}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
If sending a notification raises an error, the server logs it to stderr and the
|
|
354
|
+
call and its result are unaffected.
|
|
355
|
+
|
|
312
356
|
## Forward-compatibility example
|
|
313
357
|
|
|
314
358
|
If a future `codex exec` release adds a new flag (say `--super-mode`), use it
|
|
@@ -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
|
|
@@ -274,6 +293,31 @@ buffered output and reap the process, so the effective ceiling is
|
|
|
274
293
|
`timeout_seconds + 20`. A timed-out call is flagged `isError` and carries
|
|
275
294
|
whatever `codex` had printed so far.
|
|
276
295
|
|
|
296
|
+
An MCP client may give up on a call that has sent neither a response nor a
|
|
297
|
+
progress notification for a while. Claude Code 2.1.318 does so after 1800
|
|
298
|
+
seconds by default for a stdio server ("sent no response or progress for
|
|
299
|
+
1800s"), and a long `codex` turn can take longer than that. While `codex` runs,
|
|
300
|
+
the server therefore sends a `notifications/progress` message every 60 seconds,
|
|
301
|
+
so the call lasts until `codex` finishes or `timeout_seconds` runs out. A client
|
|
302
|
+
gets these only when it sent a `progressToken` with the call, as Claude Code
|
|
303
|
+
does; a client that gives up after less than 60 seconds without progress is not
|
|
304
|
+
covered. With the default `timeout_seconds` of 1800 the call still ends after
|
|
305
|
+
about 30 minutes, at the server's own timeout, so to let `codex` run longer,
|
|
306
|
+
raise `timeout_seconds` (30 to 3600 per call) or `$CODEX_CLI_MCP_SLIM_TIMEOUT`,
|
|
307
|
+
which sets the default. In the notification's `params`, `progress` counts the
|
|
308
|
+
notifications sent so far and `message` says how long `codex` has been running:
|
|
309
|
+
|
|
310
|
+
```json
|
|
311
|
+
{
|
|
312
|
+
"jsonrpc": "2.0",
|
|
313
|
+
"method": "notifications/progress",
|
|
314
|
+
"params": {"progressToken": 7, "progress": 2, "message": "codex running for 120s"}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
If sending a notification raises an error, the server logs it to stderr and the
|
|
319
|
+
call and its result are unaffected.
|
|
320
|
+
|
|
277
321
|
## Forward-compatibility example
|
|
278
322
|
|
|
279
323
|
If a future `codex exec` release adds a new flag (say `--super-mode`), use it
|
|
@@ -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.2"
|
|
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,12 @@ 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
|
|
26
|
+
from collections.abc import Awaitable, Callable
|
|
25
27
|
from importlib.metadata import version as _pkg_version
|
|
26
|
-
from typing import Any
|
|
28
|
+
from typing import Any, NamedTuple
|
|
27
29
|
|
|
28
30
|
from mcp.server import Server, ServerRequestContext
|
|
29
31
|
from mcp.server.stdio import stdio_server
|
|
@@ -41,6 +43,16 @@ logger = logging.getLogger("codex_cli_mcp_slim")
|
|
|
41
43
|
CODEX_CMD = os.environ.get("CODEX_CMD", "codex")
|
|
42
44
|
DEFAULT_TIMEOUT = int(os.environ.get("CODEX_CLI_MCP_SLIM_TIMEOUT", "1800"))
|
|
43
45
|
|
|
46
|
+
# Seconds between the progress notifications _run_codex sends while codex runs. An MCP client
|
|
47
|
+
# may abort a call that sends neither a response nor progress for a while (Claude Code does
|
|
48
|
+
# so after 1800s by default: "sent no response or progress for 1800s"), and a long codex turn
|
|
49
|
+
# outlasts that; the notifications are what keeps such a call alive.
|
|
50
|
+
PROGRESS_INTERVAL = 60
|
|
51
|
+
|
|
52
|
+
# ServerSession.report_progress, or anything called the same way. It does nothing when the
|
|
53
|
+
# client sent no progress token with the call.
|
|
54
|
+
_ReportProgress = Callable[..., Awaitable[None]]
|
|
55
|
+
|
|
44
56
|
_PKG_NAME = "codex-cli-mcp-slim"
|
|
45
57
|
|
|
46
58
|
# Flags placed right after `codex exec` on every invocation. main() fills this from the
|
|
@@ -49,12 +61,89 @@ _PKG_NAME = "codex-cli-mcp-slim"
|
|
|
49
61
|
#
|
|
50
62
|
# uvx codex-cli-mcp-slim -c model_reasoning_effort=high -C /srv/scratch
|
|
51
63
|
#
|
|
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.
|
|
64
|
+
# `-c` and `--add-dir` may repeat (for `-c` the last one wins), so a per-call `config` can
|
|
65
|
+
# override a server-level `-c`. The flags in _SINGLE_USE_FLAGS may not: see there.
|
|
56
66
|
SERVER_ARGS: list[str] = []
|
|
57
67
|
|
|
68
|
+
# Per-call parameters whose flag codex accepts only once, with every spelling of that flag.
|
|
69
|
+
# A second occurrence makes `codex exec` exit 2 with "cannot be used multiple times", so when
|
|
70
|
+
# the server's own command line already carries one of these flags, the per-call parameter
|
|
71
|
+
# is left out of argv and the server-level value is used: whoever started the server fixed
|
|
72
|
+
# that configuration on purpose.
|
|
73
|
+
_SINGLE_USE_FLAGS: dict[str, tuple[str, ...]] = {
|
|
74
|
+
"cd": ("-C", "--cd"),
|
|
75
|
+
"model": ("-m", "--model"),
|
|
76
|
+
"sandbox": ("-s", "--sandbox"),
|
|
77
|
+
"profile": ("-p", "--profile"),
|
|
78
|
+
"ephemeral": ("--ephemeral",),
|
|
79
|
+
"skip_git_repo_check": ("--skip-git-repo-check",),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# The `codex exec` options (codex-cli 0.159.2) that take the next token as their value, so
|
|
84
|
+
# _params_set_by reads that token as a value and never as a flag of its own.
|
|
85
|
+
_VALUE_FLAGS = frozenset(
|
|
86
|
+
{
|
|
87
|
+
*("-c", "--config", "--enable", "--disable", "-i", "--image"),
|
|
88
|
+
*("-m", "--model", "--local-provider", "-p", "--profile", "-s", "--sandbox"),
|
|
89
|
+
*("-C", "--cd", "--add-dir", "--thread-source", "--output-schema", "--color"),
|
|
90
|
+
*("-o", "--output-last-message"),
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class _ServerSetting(NamedTuple):
|
|
96
|
+
"""How the server's own command line sets one of the _SINGLE_USE_FLAGS parameters."""
|
|
97
|
+
|
|
98
|
+
value: str | bool # the flag's value, or True for a flag without one
|
|
99
|
+
written: str # the flag as that command line spells it, for example `-C /srv`
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _params_set_by(server_args: list[str]) -> dict[str, _ServerSetting]:
|
|
103
|
+
"""The _SINGLE_USE_FLAGS parameters whose flag already appears in `server_args`.
|
|
104
|
+
|
|
105
|
+
A flag counts in every form codex parses as that flag: `--cd DIR`, `--cd=DIR`, `-C DIR`
|
|
106
|
+
and the attached short forms `-CDIR` and `-C=DIR`. Short flags are case-sensitive, so the
|
|
107
|
+
repeatable `-c` is not mistaken for `-C`. The token after a _VALUE_FLAGS option is its
|
|
108
|
+
value (`-m` in `-c -m` is not the model flag), and nothing after `--` is a flag.
|
|
109
|
+
"""
|
|
110
|
+
held: dict[str, _ServerSetting] = {}
|
|
111
|
+
tokens = iter(server_args)
|
|
112
|
+
for token in tokens:
|
|
113
|
+
if token == "--":
|
|
114
|
+
break
|
|
115
|
+
if token in _VALUE_FLAGS:
|
|
116
|
+
value = next(tokens, None)
|
|
117
|
+
if value is None:
|
|
118
|
+
break
|
|
119
|
+
param = next((p for p, flags in _SINGLE_USE_FLAGS.items() if token in flags), None)
|
|
120
|
+
if param is not None:
|
|
121
|
+
held.setdefault(param, _ServerSetting(value, shlex.join([token, value])))
|
|
122
|
+
continue
|
|
123
|
+
for param, spellings in _SINGLE_USE_FLAGS.items():
|
|
124
|
+
for flag in spellings:
|
|
125
|
+
if token == flag:
|
|
126
|
+
held.setdefault(param, _ServerSetting(True, token))
|
|
127
|
+
elif len(flag) == 2 and token.startswith(flag):
|
|
128
|
+
value = token[2:].removeprefix("=")
|
|
129
|
+
held.setdefault(param, _ServerSetting(value, shlex.quote(token)))
|
|
130
|
+
elif token.startswith(flag + "="):
|
|
131
|
+
value = token[len(flag) + 1 :]
|
|
132
|
+
held.setdefault(param, _ServerSetting(value, shlex.quote(token)))
|
|
133
|
+
return held
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# What SERVER_ARGS sets among the _SINGLE_USE_FLAGS parameters. _set_server_args parses it
|
|
137
|
+
# once, together with SERVER_ARGS, so _build_argv, _invoke and on_list_tools read one answer.
|
|
138
|
+
_SERVER_SET: dict[str, _ServerSetting] = {}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _set_server_args(server_args: list[str]) -> None:
|
|
142
|
+
global SERVER_ARGS, _SERVER_SET
|
|
143
|
+
SERVER_ARGS = list(server_args)
|
|
144
|
+
_SERVER_SET = _params_set_by(SERVER_ARGS)
|
|
145
|
+
|
|
146
|
+
|
|
58
147
|
# The Server is built at the bottom of this file: v2 takes the handlers as constructor
|
|
59
148
|
# arguments, so they must already be defined by the time it is constructed.
|
|
60
149
|
|
|
@@ -73,8 +162,11 @@ def _build_argv(
|
|
|
73
162
|
extra_args: list[str] | None = None,
|
|
74
163
|
server_args: list[str] | None = None,
|
|
75
164
|
) -> list[str]:
|
|
76
|
-
|
|
77
|
-
|
|
165
|
+
if server_args is None:
|
|
166
|
+
server_args, server_set = SERVER_ARGS, _SERVER_SET
|
|
167
|
+
else:
|
|
168
|
+
server_set = _params_set_by(server_args)
|
|
169
|
+
argv: list[str] = [CODEX_CMD, "exec", *server_args]
|
|
78
170
|
if thread_id:
|
|
79
171
|
# `codex exec resume <id>` is a subcommand: it has to come before the per-call flags,
|
|
80
172
|
# and it accepts only a subset of them (the `codex-reply` schema advertises just that
|
|
@@ -82,21 +174,25 @@ def _build_argv(
|
|
|
82
174
|
# `exec` options there, so a server pinned to a working directory keeps that
|
|
83
175
|
# directory for replies as well.
|
|
84
176
|
argv += ["resume", thread_id]
|
|
85
|
-
|
|
177
|
+
# Per-call flags after `resume` are the subcommand's own, and codex accepts them there
|
|
178
|
+
# even when the same flag sits among the server-level ones, so only a plain `exec` call
|
|
179
|
+
# leaves out what the server already sets.
|
|
180
|
+
held = {} if thread_id else server_set
|
|
181
|
+
if cd and "cd" not in held:
|
|
86
182
|
argv += ["-C", cd]
|
|
87
|
-
if model:
|
|
183
|
+
if model and "model" not in held:
|
|
88
184
|
argv += ["-m", model]
|
|
89
185
|
for item in config or []:
|
|
90
186
|
argv += ["-c", item] # repeatable flag: one -c per key=value
|
|
91
|
-
if sandbox:
|
|
187
|
+
if sandbox and "sandbox" not in held:
|
|
92
188
|
argv += ["--sandbox", sandbox]
|
|
93
189
|
for d in add_dir or []:
|
|
94
190
|
argv += ["--add-dir", d] # repeatable flag: one --add-dir per directory
|
|
95
|
-
if profile:
|
|
191
|
+
if profile and "profile" not in held:
|
|
96
192
|
argv += ["-p", profile]
|
|
97
|
-
if ephemeral:
|
|
193
|
+
if ephemeral and "ephemeral" not in held:
|
|
98
194
|
argv.append("--ephemeral")
|
|
99
|
-
if skip_git_repo_check:
|
|
195
|
+
if skip_git_repo_check and "skip_git_repo_check" not in held:
|
|
100
196
|
argv.append("--skip-git-repo-check")
|
|
101
197
|
if extra_args:
|
|
102
198
|
argv += list(extra_args)
|
|
@@ -115,6 +211,7 @@ async def _run_codex(
|
|
|
115
211
|
prompt: str,
|
|
116
212
|
timeout: int,
|
|
117
213
|
env_overrides: dict[str, str] | None = None,
|
|
214
|
+
report_progress: _ReportProgress | None = None,
|
|
118
215
|
) -> dict[str, Any]:
|
|
119
216
|
env = os.environ.copy()
|
|
120
217
|
if env_overrides:
|
|
@@ -145,6 +242,11 @@ async def _run_codex(
|
|
|
145
242
|
|
|
146
243
|
# shield the task so a first-wait timeout doesn't cancel the second wait below
|
|
147
244
|
communicate_task = asyncio.ensure_future(proc.communicate(prompt.encode()))
|
|
245
|
+
progress_task = (
|
|
246
|
+
asyncio.ensure_future(_report_progress_every(PROGRESS_INTERVAL, report_progress))
|
|
247
|
+
if report_progress is not None
|
|
248
|
+
else None
|
|
249
|
+
)
|
|
148
250
|
try:
|
|
149
251
|
try:
|
|
150
252
|
stdout, stderr = await asyncio.wait_for(
|
|
@@ -183,10 +285,25 @@ async def _run_codex(
|
|
|
183
285
|
_kill_process_group(proc)
|
|
184
286
|
raise
|
|
185
287
|
finally:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
288
|
+
# The progress task ends before the result goes back, since the protocol has progress
|
|
289
|
+
# stop once the call completes; cancelling it also ends a notification stuck on a
|
|
290
|
+
# client that has stopped reading. Both tasks are cancelled before anything is
|
|
291
|
+
# awaited, and the wait is repeated until they have finished. The SDK cancels a call
|
|
292
|
+
# through an anyio cancel scope, which cancels this coroutine again at every await, so
|
|
293
|
+
# a single await here could be cut short and leave communicate() running for as long
|
|
294
|
+
# as a descendant outside the process group keeps the pipes open. A cancellation that
|
|
295
|
+
# arrives during the wait is raised once the tasks are done.
|
|
296
|
+
leftovers = {t for t in (progress_task, communicate_task) if t is not None and not t.done()}
|
|
297
|
+
for task in leftovers:
|
|
298
|
+
task.cancel()
|
|
299
|
+
interrupted: asyncio.CancelledError | None = None
|
|
300
|
+
while leftovers := {t for t in leftovers if not t.done()}:
|
|
301
|
+
try:
|
|
302
|
+
await asyncio.wait(leftovers)
|
|
303
|
+
except asyncio.CancelledError as exc:
|
|
304
|
+
interrupted = exc
|
|
305
|
+
if interrupted is not None:
|
|
306
|
+
raise interrupted
|
|
190
307
|
|
|
191
308
|
return {
|
|
192
309
|
"ok": proc.returncode == 0,
|
|
@@ -197,6 +314,27 @@ async def _run_codex(
|
|
|
197
314
|
}
|
|
198
315
|
|
|
199
316
|
|
|
317
|
+
async def _report_progress_every(interval: float, report_progress: _ReportProgress) -> None:
|
|
318
|
+
"""Send a progress notification every `interval` seconds until cancelled.
|
|
319
|
+
|
|
320
|
+
`progress` counts the notifications, so it rises with each one as the protocol requires;
|
|
321
|
+
the message carries the time codex has been running. A notification that raises is logged
|
|
322
|
+
and the next one is still sent: it must not affect the codex run or its result. (The SDK
|
|
323
|
+
itself drops a notification on a closed transport, with a debug log and no exception.)
|
|
324
|
+
"""
|
|
325
|
+
loop = asyncio.get_running_loop()
|
|
326
|
+
started = loop.time()
|
|
327
|
+
count = 0
|
|
328
|
+
while True:
|
|
329
|
+
await asyncio.sleep(interval)
|
|
330
|
+
count += 1
|
|
331
|
+
elapsed = round(loop.time() - started)
|
|
332
|
+
try:
|
|
333
|
+
await report_progress(count, message=f"codex running for {elapsed}s")
|
|
334
|
+
except Exception as exc:
|
|
335
|
+
logger.warning("progress notification failed: %s: %s", type(exc).__name__, exc)
|
|
336
|
+
|
|
337
|
+
|
|
200
338
|
def _kill_process_group(proc: asyncio.subprocess.Process) -> None:
|
|
201
339
|
"""Kill codex's process group; a descendant that setsid/setpgid away from it is out of reach."""
|
|
202
340
|
if sys.platform != "win32":
|
|
@@ -572,7 +710,12 @@ def _error_result(text: str) -> CallToolResult:
|
|
|
572
710
|
return CallToolResult(content=[TextContent(type="text", text=f"[ERROR] {text}")], is_error=True)
|
|
573
711
|
|
|
574
712
|
|
|
575
|
-
async def _invoke(
|
|
713
|
+
async def _invoke(
|
|
714
|
+
arguments: dict[str, Any] | None,
|
|
715
|
+
*,
|
|
716
|
+
reply: bool,
|
|
717
|
+
report_progress: _ReportProgress | None = None,
|
|
718
|
+
) -> CallToolResult:
|
|
576
719
|
"""The body of both tools, taking a plain dict so callers need no SDK request object."""
|
|
577
720
|
args = dict(arguments or {})
|
|
578
721
|
|
|
@@ -605,14 +748,37 @@ async def _invoke(arguments: dict[str, Any] | None, *, reply: bool) -> CallToolR
|
|
|
605
748
|
prompt=args["prompt"],
|
|
606
749
|
timeout=int(args.get("timeout_seconds") or DEFAULT_TIMEOUT),
|
|
607
750
|
env_overrides=args.get("env"),
|
|
751
|
+
report_progress=report_progress,
|
|
608
752
|
)
|
|
609
753
|
parsed = _parse_events(result.get("stdout", ""))
|
|
610
754
|
text = _format_result(result)
|
|
755
|
+
# Name what _build_argv left out, and the value used instead, so a caller does not take a
|
|
756
|
+
# run in the server's directory (say) for a run in the one it asked for.
|
|
757
|
+
held = {} if reply else _SERVER_SET
|
|
758
|
+
dropped = [param for param in _SINGLE_USE_FLAGS if param in held and args.get(param)]
|
|
611
759
|
structured: dict[str, Any] | None = None
|
|
612
760
|
if parsed["thread_id"]:
|
|
613
761
|
# The same shape the deprecated `codex mcp-server` returned, so a client that reads
|
|
614
762
|
# structuredContent.threadId keeps working.
|
|
615
763
|
structured = {"threadId": parsed["thread_id"], "content": _response_text(parsed)}
|
|
764
|
+
if dropped:
|
|
765
|
+
lines = [
|
|
766
|
+
f"{p}={json.dumps(args[p], ensure_ascii=False)}; "
|
|
767
|
+
f"this server runs with {held[p].written}"
|
|
768
|
+
for p in dropped
|
|
769
|
+
]
|
|
770
|
+
logger.warning("per-call parameters ignored: %s", " | ".join(lines))
|
|
771
|
+
text += (
|
|
772
|
+
"\n\nignored, because this server's own command line already sets the flag "
|
|
773
|
+
"and codex takes it only once:\n" + "\n".join(lines)
|
|
774
|
+
)
|
|
775
|
+
# A client that reads only structuredContent learns it here.
|
|
776
|
+
structured = {
|
|
777
|
+
**(structured or {}),
|
|
778
|
+
"ignoredParameters": [
|
|
779
|
+
{"name": p, "value": args[p], "serverValue": held[p].value} for p in dropped
|
|
780
|
+
],
|
|
781
|
+
}
|
|
616
782
|
return CallToolResult(
|
|
617
783
|
content=[TextContent(type="text", text=text)],
|
|
618
784
|
structured_content=structured,
|
|
@@ -620,29 +786,54 @@ async def _invoke(arguments: dict[str, Any] | None, *, reply: bool) -> CallToolR
|
|
|
620
786
|
)
|
|
621
787
|
|
|
622
788
|
|
|
623
|
-
async def codex(
|
|
624
|
-
|
|
789
|
+
async def codex(
|
|
790
|
+
arguments: dict[str, Any] | None, *, report_progress: _ReportProgress | None = None
|
|
791
|
+
) -> CallToolResult:
|
|
792
|
+
return await _invoke(arguments, reply=False, report_progress=report_progress)
|
|
625
793
|
|
|
626
794
|
|
|
627
|
-
async def codex_reply(
|
|
628
|
-
|
|
795
|
+
async def codex_reply(
|
|
796
|
+
arguments: dict[str, Any] | None, *, report_progress: _ReportProgress | None = None
|
|
797
|
+
) -> CallToolResult:
|
|
798
|
+
return await _invoke(arguments, reply=True, report_progress=report_progress)
|
|
629
799
|
|
|
630
800
|
|
|
631
801
|
async def on_list_tools(
|
|
632
802
|
ctx: ServerRequestContext, params: PaginatedRequestParams | None
|
|
633
803
|
) -> ListToolsResult:
|
|
634
|
-
|
|
804
|
+
# The caller cannot see the server's command line, so the `codex` schema says which
|
|
805
|
+
# parameters it already fixes, rather than inviting values that _build_argv would drop.
|
|
806
|
+
if not _SERVER_SET:
|
|
807
|
+
return ListToolsResult(tools=[_CODEX_TOOL, _REPLY_TOOL])
|
|
808
|
+
props = dict(_CODEX_PROPS)
|
|
809
|
+
for param, setting in _SERVER_SET.items():
|
|
810
|
+
props[param] = {
|
|
811
|
+
**props[param],
|
|
812
|
+
"description": (
|
|
813
|
+
f"{props[param]['description']} This server's own command line already "
|
|
814
|
+
f"sets {setting.written}, so a value passed here is ignored."
|
|
815
|
+
),
|
|
816
|
+
}
|
|
817
|
+
codex_tool = _CODEX_TOOL.model_copy(
|
|
818
|
+
update={"input_schema": {**_CODEX_TOOL.input_schema, "properties": props}}
|
|
819
|
+
)
|
|
820
|
+
return ListToolsResult(tools=[codex_tool, _REPLY_TOOL])
|
|
635
821
|
|
|
636
822
|
|
|
637
|
-
async def on_call_tool(
|
|
823
|
+
async def on_call_tool(
|
|
824
|
+
ctx: ServerRequestContext | None, params: CallToolRequestParams
|
|
825
|
+
) -> CallToolResult:
|
|
638
826
|
# An escaping exception would become a JSON-RPC error, which clients treat as a transport
|
|
639
827
|
# failure rather than as something to show the model. CancelledError derives from
|
|
640
828
|
# BaseException, so peer cancellation still propagates and _run_codex's killpg still runs.
|
|
829
|
+
# The session is scoped to this request, so its progress goes out against this call's
|
|
830
|
+
# token. Tests and direct callers pass no context; such a call simply sends no progress.
|
|
831
|
+
report_progress = ctx.session.report_progress if ctx is not None else None
|
|
641
832
|
try:
|
|
642
833
|
if params.name == "codex":
|
|
643
|
-
return await codex(params.arguments)
|
|
834
|
+
return await codex(params.arguments, report_progress=report_progress)
|
|
644
835
|
if params.name == "codex-reply":
|
|
645
|
-
return await codex_reply(params.arguments)
|
|
836
|
+
return await codex_reply(params.arguments, report_progress=report_progress)
|
|
646
837
|
return _error_result(f"unknown tool: {params.name}")
|
|
647
838
|
except Exception as exc:
|
|
648
839
|
logger.exception("%s failed", params.name)
|
|
@@ -691,7 +882,7 @@ def main() -> None:
|
|
|
691
882
|
if sys.argv[1:] == ["--version"]:
|
|
692
883
|
print(f"{_PKG_NAME} {_pkg_version(_PKG_NAME)}")
|
|
693
884
|
return
|
|
694
|
-
|
|
885
|
+
_set_server_args(sys.argv[1:])
|
|
695
886
|
asyncio.run(_amain())
|
|
696
887
|
|
|
697
888
|
|
{codex_cli_mcp_slim-0.1.0 → codex_cli_mcp_slim-0.1.2/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.2
|
|
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
|
|
@@ -309,6 +328,31 @@ buffered output and reap the process, so the effective ceiling is
|
|
|
309
328
|
`timeout_seconds + 20`. A timed-out call is flagged `isError` and carries
|
|
310
329
|
whatever `codex` had printed so far.
|
|
311
330
|
|
|
331
|
+
An MCP client may give up on a call that has sent neither a response nor a
|
|
332
|
+
progress notification for a while. Claude Code 2.1.318 does so after 1800
|
|
333
|
+
seconds by default for a stdio server ("sent no response or progress for
|
|
334
|
+
1800s"), and a long `codex` turn can take longer than that. While `codex` runs,
|
|
335
|
+
the server therefore sends a `notifications/progress` message every 60 seconds,
|
|
336
|
+
so the call lasts until `codex` finishes or `timeout_seconds` runs out. A client
|
|
337
|
+
gets these only when it sent a `progressToken` with the call, as Claude Code
|
|
338
|
+
does; a client that gives up after less than 60 seconds without progress is not
|
|
339
|
+
covered. With the default `timeout_seconds` of 1800 the call still ends after
|
|
340
|
+
about 30 minutes, at the server's own timeout, so to let `codex` run longer,
|
|
341
|
+
raise `timeout_seconds` (30 to 3600 per call) or `$CODEX_CLI_MCP_SLIM_TIMEOUT`,
|
|
342
|
+
which sets the default. In the notification's `params`, `progress` counts the
|
|
343
|
+
notifications sent so far and `message` says how long `codex` has been running:
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
{
|
|
347
|
+
"jsonrpc": "2.0",
|
|
348
|
+
"method": "notifications/progress",
|
|
349
|
+
"params": {"progressToken": 7, "progress": 2, "message": "codex running for 120s"}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
If sending a notification raises an error, the server logs it to stderr and the
|
|
354
|
+
call and its result are unaffected.
|
|
355
|
+
|
|
312
356
|
## Forward-compatibility example
|
|
313
357
|
|
|
314
358
|
If a future `codex exec` release adds a new flag (say `--super-mode`), use it
|