codex-cli-mcp-slim 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ """codex_cli_mcp_slim - thin auditable MCP wrapper around the Codex CLI (`codex exec`)."""
2
+
3
+ from importlib.metadata import version as _pkg_version
4
+
5
+ from .server import main
6
+
7
+ __version__ = _pkg_version("codex-cli-mcp-slim")
8
+ __all__ = ["__version__", "main"]
File without changes
@@ -0,0 +1,699 @@
1
+ """codex_cli_mcp_slim: Thin, auditable MCP server wrapping the Codex CLI (`codex exec`).
2
+
3
+ Design goals:
4
+ * Auditable: a single file with only one third-party dependency (`mcp`),
5
+ readable end-to-end in one sitting.
6
+ * Same tool names: the two tools are `codex` and `codex-reply`, the names the
7
+ deprecated `codex mcp-server` exposed, so an MCP client configured for that
8
+ server keeps its server and tool names after swapping the launch command.
9
+ * Forward-compatible: any `codex exec` flag is reachable via `extra_args`
10
+ without server changes; the binary itself can be swapped via $CODEX_CMD.
11
+ * Safe: subprocess uses argv-list form (no shell), the prompt travels over
12
+ stdin rather than argv, hard timeout, explicit env merge. Each invocation
13
+ logs the exact argv to stderr.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import contextlib
20
+ import json
21
+ import logging
22
+ import os
23
+ import signal
24
+ import sys
25
+ from importlib.metadata import version as _pkg_version
26
+ from typing import Any
27
+
28
+ from mcp.server import Server, ServerRequestContext
29
+ from mcp.server.stdio import stdio_server
30
+ from mcp.types import (
31
+ CallToolRequestParams,
32
+ CallToolResult,
33
+ ListToolsResult,
34
+ PaginatedRequestParams,
35
+ TextContent,
36
+ Tool,
37
+ )
38
+
39
+ logger = logging.getLogger("codex_cli_mcp_slim")
40
+
41
+ CODEX_CMD = os.environ.get("CODEX_CMD", "codex")
42
+ DEFAULT_TIMEOUT = int(os.environ.get("CODEX_CLI_MCP_SLIM_TIMEOUT", "1800"))
43
+
44
+ _PKG_NAME = "codex-cli-mcp-slim"
45
+
46
+ # Flags placed right after `codex exec` on every invocation. main() fills this from the
47
+ # server's own command line, so one MCP-client entry can pin a reasoning effort, a working
48
+ # directory or a model for every call it makes:
49
+ #
50
+ # uvx codex-cli-mcp-slim -c model_reasoning_effort=high -C /srv/scratch
51
+ #
52
+ # `-c` may repeat and the last one wins, so a per-call `config` can override a server-level
53
+ # `-c`. Single-value flags such as `-m` and `-C` may not repeat: codex rejects the second
54
+ # one, and the tool result carries that error. Keep server-level flags and per-call
55
+ # parameters disjoint for those.
56
+ SERVER_ARGS: list[str] = []
57
+
58
+ # The Server is built at the bottom of this file: v2 takes the handlers as constructor
59
+ # arguments, so they must already be defined by the time it is constructed.
60
+
61
+
62
+ def _build_argv(
63
+ *,
64
+ thread_id: str | None = None,
65
+ cd: str | None = None,
66
+ model: str | None = None,
67
+ config: list[str] | None = None,
68
+ sandbox: str | None = None,
69
+ add_dir: list[str] | None = None,
70
+ profile: str | None = None,
71
+ ephemeral: bool = False,
72
+ skip_git_repo_check: bool = False,
73
+ extra_args: list[str] | None = None,
74
+ server_args: list[str] | None = None,
75
+ ) -> list[str]:
76
+ argv: list[str] = [CODEX_CMD, "exec"]
77
+ argv += SERVER_ARGS if server_args is None else server_args
78
+ if thread_id:
79
+ # `codex exec resume <id>` is a subcommand: it has to come before the per-call flags,
80
+ # and it accepts only a subset of them (the `codex-reply` schema advertises just that
81
+ # subset). Server-level flags stay in front of it on purpose: codex parses them as
82
+ # `exec` options there, so a server pinned to a working directory keeps that
83
+ # directory for replies as well.
84
+ argv += ["resume", thread_id]
85
+ if cd:
86
+ argv += ["-C", cd]
87
+ if model:
88
+ argv += ["-m", model]
89
+ for item in config or []:
90
+ argv += ["-c", item] # repeatable flag: one -c per key=value
91
+ if sandbox:
92
+ argv += ["--sandbox", sandbox]
93
+ for d in add_dir or []:
94
+ argv += ["--add-dir", d] # repeatable flag: one --add-dir per directory
95
+ if profile:
96
+ argv += ["-p", profile]
97
+ if ephemeral:
98
+ argv.append("--ephemeral")
99
+ if skip_git_repo_check:
100
+ argv.append("--skip-git-repo-check")
101
+ if extra_args:
102
+ argv += list(extra_args)
103
+ # --json is not optional for this server: the thread id, the failure of a turn and the
104
+ # token usage exist only in the event stream. `-` makes codex read the prompt from
105
+ # stdin, so the prompt never appears in the process list and is not bounded by the
106
+ # argv size limit. Both stay last so that nothing in extra_args can capture `-` as its
107
+ # value.
108
+ argv += ["--json", "-"]
109
+ return argv
110
+
111
+
112
+ async def _run_codex(
113
+ *,
114
+ argv: list[str],
115
+ prompt: str,
116
+ timeout: int,
117
+ env_overrides: dict[str, str] | None = None,
118
+ ) -> dict[str, Any]:
119
+ env = os.environ.copy()
120
+ if env_overrides:
121
+ env.update({k: str(v) for k, v in env_overrides.items()})
122
+
123
+ logger.info("exec %s (timeout=%ss, prompt=%d chars)", argv, timeout, len(prompt))
124
+
125
+ # stdin is a pipe of our own: the prompt goes down it and it is closed. The child must
126
+ # never inherit the parent's stdin, which under the stdio MCP transport is the JSON-RPC
127
+ # channel from the client; a child reading from that shared file description would
128
+ # corrupt the channel and silently kill the server.
129
+ try:
130
+ proc = await asyncio.create_subprocess_exec(
131
+ *argv,
132
+ stdin=asyncio.subprocess.PIPE,
133
+ stdout=asyncio.subprocess.PIPE,
134
+ stderr=asyncio.subprocess.PIPE,
135
+ env=env,
136
+ # own session, so timeout cleanup can kill the group (POSIX only, no-op elsewhere)
137
+ start_new_session=True,
138
+ )
139
+ except OSError as exc:
140
+ return {
141
+ "ok": False,
142
+ "error": f"failed to launch codex binary: {exc} (check $CODEX_CMD)",
143
+ "argv": argv,
144
+ }
145
+
146
+ # shield the task so a first-wait timeout doesn't cancel the second wait below
147
+ communicate_task = asyncio.ensure_future(proc.communicate(prompt.encode()))
148
+ try:
149
+ try:
150
+ stdout, stderr = await asyncio.wait_for(
151
+ asyncio.shield(communicate_task), timeout=timeout
152
+ )
153
+ except asyncio.TimeoutError:
154
+ if communicate_task.done():
155
+ # narrow race: the process finished at the exact moment wait_for's timeout fired
156
+ stdout, stderr = await communicate_task
157
+ return {
158
+ "ok": proc.returncode == 0,
159
+ "returncode": proc.returncode,
160
+ "stdout": stdout.decode("utf-8", errors="replace"),
161
+ "stderr": stderr.decode("utf-8", errors="replace"),
162
+ "argv": argv,
163
+ }
164
+ _kill_process_group(proc)
165
+ stdout, stderr = b"", b""
166
+ with contextlib.suppress(asyncio.TimeoutError):
167
+ # kill above closes the pipes, so communicate() reaches EOF with the buffered output
168
+ stdout, stderr = await asyncio.wait_for(
169
+ asyncio.shield(communicate_task), timeout=10
170
+ )
171
+ # bounded backstop: a D-state descendant can't be force-killed, so don't block forever
172
+ with contextlib.suppress(asyncio.TimeoutError):
173
+ await asyncio.wait_for(proc.wait(), timeout=10)
174
+ return {
175
+ "ok": False,
176
+ "error": f"timeout after {timeout}s",
177
+ "argv": argv,
178
+ "returncode": proc.returncode,
179
+ "stdout": stdout.decode("utf-8", errors="replace"),
180
+ "stderr": stderr.decode("utf-8", errors="replace"),
181
+ }
182
+ except asyncio.CancelledError:
183
+ _kill_process_group(proc)
184
+ raise
185
+ finally:
186
+ if not communicate_task.done():
187
+ communicate_task.cancel()
188
+ with contextlib.suppress(Exception, asyncio.CancelledError):
189
+ await communicate_task
190
+
191
+ return {
192
+ "ok": proc.returncode == 0,
193
+ "returncode": proc.returncode,
194
+ "stdout": stdout.decode("utf-8", errors="replace"),
195
+ "stderr": stderr.decode("utf-8", errors="replace"),
196
+ "argv": argv,
197
+ }
198
+
199
+
200
+ def _kill_process_group(proc: asyncio.subprocess.Process) -> None:
201
+ """Kill codex's process group; a descendant that setsid/setpgid away from it is out of reach."""
202
+ if sys.platform != "win32":
203
+ # start_new_session=True makes codex pid==pgid, so killpg(proc.pid) works after it exits too
204
+ with contextlib.suppress(OSError):
205
+ os.killpg(proc.pid, signal.SIGKILL)
206
+ # any killpg failure (not just the two anticipated OSError subclasses) must still fall back here
207
+ with contextlib.suppress(OSError):
208
+ proc.kill()
209
+
210
+
211
+ def _parse_events(stdout: str) -> dict[str, Any]:
212
+ """Fold the `codex exec --json` event stream into the few facts this server reports.
213
+
214
+ The stream is one JSON object per line. Lines that are not JSON objects are skipped
215
+ rather than rejected: the events this server needs are self-describing by `type`, and
216
+ a future codex release adding lines it does not know must not turn every call into an
217
+ error.
218
+ """
219
+ thread_id: str | None = None
220
+ messages: list[str] = []
221
+ errors: list[str] = []
222
+ usage: dict[str, Any] | None = None
223
+ turn_status: str | None = None
224
+ for line in stdout.splitlines():
225
+ line = line.strip()
226
+ if not line:
227
+ continue
228
+ try:
229
+ event = json.loads(line)
230
+ except ValueError:
231
+ continue
232
+ if not isinstance(event, dict):
233
+ continue
234
+ kind = event.get("type")
235
+ if kind == "thread.started":
236
+ thread_id = event.get("thread_id") or thread_id
237
+ elif kind == "item.completed":
238
+ item = event.get("item")
239
+ if isinstance(item, dict) and item.get("type") == "agent_message":
240
+ messages.append(str(item.get("text") or ""))
241
+ elif kind == "turn.completed":
242
+ turn_status = "completed"
243
+ usage = event.get("usage") if isinstance(event.get("usage"), dict) else None
244
+ elif kind == "turn.failed":
245
+ turn_status = "failed"
246
+ error = event.get("error")
247
+ message = error.get("message") if isinstance(error, dict) else None
248
+ errors.append(str(message or error or event))
249
+ elif kind == "error":
250
+ errors.append(str(event.get("message") or event))
251
+ # codex reports one failure twice, as an `error` event and again inside `turn.failed`.
252
+ unique_errors = list(dict.fromkeys(errors))
253
+ return {
254
+ "thread_id": thread_id,
255
+ "messages": messages,
256
+ "errors": unique_errors,
257
+ "usage": usage,
258
+ "turn_status": turn_status,
259
+ "saw_events": bool(thread_id or messages or unique_errors or turn_status),
260
+ }
261
+
262
+
263
+ def _codex_trailer(parsed: dict[str, Any]) -> str:
264
+ """One line of run metadata, appended under codex's own text.
265
+
266
+ `thread_id` is here because `codex-reply` needs it and the event stream is the only
267
+ place codex reports it. `status` is the turn's own verdict, which the exit code does
268
+ not carry on its own: a turn that failed inside the model API still exits 0 when the
269
+ process shut down cleanly (observed with an unsupported reasoning effort).
270
+ """
271
+ bits = [f"thread_id={parsed.get('thread_id') or 'UNKNOWN'}"]
272
+ bits.append(f"status={parsed.get('turn_status') or 'UNKNOWN'}")
273
+ usage = parsed.get("usage") or {}
274
+ for key in ("input_tokens", "cached_input_tokens", "output_tokens"):
275
+ if usage.get(key) is not None:
276
+ bits.append(f"{key}={usage[key]}")
277
+ return "[codex] " + " ".join(bits)
278
+
279
+
280
+ def _run_failed(result: dict[str, Any]) -> bool:
281
+ """Whether the client should see this as a failed tool call.
282
+
283
+ A non-zero exit is always a failure. The event stream is consulted on top of that
284
+ because it is codex's own verdict on the turn rather than the shell's: a `turn.failed`
285
+ event can arrive with exit code 0, and an `error` event without any agent message
286
+ means the model never answered.
287
+ """
288
+ if not result["ok"]:
289
+ return True
290
+ parsed = _parse_events(result.get("stdout", ""))
291
+ if parsed["turn_status"] == "failed":
292
+ return True
293
+ # The same emptiness test _format_result applies, so a run whose text opens with
294
+ # [ERROR] is never handed back with is_error unset.
295
+ return bool(parsed["errors"]) and not _response_text(parsed).strip()
296
+
297
+
298
+ def _response_text(parsed: dict[str, Any]) -> str:
299
+ # codex may emit several agent messages in one turn; the last one is the answer.
300
+ return parsed["messages"][-1] if parsed["messages"] else ""
301
+
302
+
303
+ def _format_result(result: dict[str, Any]) -> str:
304
+ parsed = _parse_events(result.get("stdout", ""))
305
+ if result["ok"] and parsed["turn_status"] != "failed":
306
+ response = _response_text(parsed)
307
+ if response.strip():
308
+ parts = [f"{response.rstrip()}\n\n{_codex_trailer(parsed)}"]
309
+ if parsed["errors"]:
310
+ # An error the turn recovered from: keep it visible without costing the answer.
311
+ parts.append("errors:\n" + "\n".join(parsed["errors"]))
312
+ return "\n\n".join(parts)
313
+ marker = "[ERROR]" if parsed["errors"] else "[WARNING]"
314
+ parts = [f"{marker} codex exited successfully but produced no agent message."]
315
+ if parsed["errors"]:
316
+ parts.append("errors:\n" + "\n".join(parsed["errors"]))
317
+ if parsed["saw_events"]:
318
+ parts.append(_codex_trailer(parsed))
319
+ elif result.get("stdout", "").strip():
320
+ parts.append(f"stdout:\n{result['stdout']}")
321
+ if result.get("stderr"):
322
+ parts.append(f"stderr:\n{result['stderr']}")
323
+ parts.append(f"argv: {result.get('argv')}")
324
+ return "\n\n".join(parts)
325
+ parts = [f"[ERROR] {result.get('error', 'codex failed')}"]
326
+ if "returncode" in result:
327
+ parts.append(f"returncode={result['returncode']}")
328
+ if parsed["errors"]:
329
+ parts.append("errors:\n" + "\n".join(parsed["errors"]))
330
+ if parsed["saw_events"]:
331
+ parts.append(_codex_trailer(parsed))
332
+ if parsed["messages"]:
333
+ parts.append(f"last agent message:\n{_response_text(parsed)}")
334
+ elif result.get("stdout"):
335
+ parts.append(f"stdout:\n{result['stdout']}")
336
+ if result.get("stderr"):
337
+ parts.append(f"stderr:\n{result['stderr']}")
338
+ parts.append(f"argv: {result.get('argv')}")
339
+ return "\n\n".join(parts)
340
+
341
+
342
+ _SANDBOX_MODES = ("read-only", "workspace-write", "danger-full-access")
343
+
344
+ _COMMON_PROPS: dict[str, Any] = {
345
+ "prompt": {
346
+ "type": "string",
347
+ "minLength": 1,
348
+ "description": (
349
+ "Prompt sent verbatim to codex on stdin. codex runs its full agentic loop "
350
+ "and this server returns the final agent message."
351
+ ),
352
+ },
353
+ "model": {
354
+ "type": "string",
355
+ "description": "Pass -m <MODEL>: the model slug for this call, overriding config.toml.",
356
+ },
357
+ "config": {
358
+ "type": "array",
359
+ "items": {"type": "string"},
360
+ "description": (
361
+ "Configuration overrides as key=value strings, one per element. Each entry maps "
362
+ 'to a separate codex -c flag (for example "model_reasoning_effort=high"). '
363
+ "Values are parsed as TOML by codex, so quote strings that are not bare words."
364
+ ),
365
+ },
366
+ "ephemeral": {
367
+ "type": "boolean",
368
+ "description": "Pass --ephemeral: do not persist the session's rollout file to disk.",
369
+ },
370
+ "skip_git_repo_check": {
371
+ "type": "boolean",
372
+ "description": (
373
+ "Pass --skip-git-repo-check: allow running in a directory that is not inside "
374
+ "a git repository. Without it codex refuses such a working directory."
375
+ ),
376
+ },
377
+ "extra_args": {
378
+ "type": "array",
379
+ "items": {"type": "string"},
380
+ "description": (
381
+ "Raw CLI flags appended verbatim (one token per element). "
382
+ "Use to reach new or uncommon codex exec flags without updating this server. "
383
+ "Do not pass --json or a prompt: the server adds both."
384
+ ),
385
+ },
386
+ "env": {
387
+ "type": "object",
388
+ "additionalProperties": {"type": "string"},
389
+ "description": "Extra environment variables for the codex subprocess.",
390
+ },
391
+ "timeout_seconds": {
392
+ "type": "integer",
393
+ "minimum": 30,
394
+ "maximum": 3600,
395
+ "description": (
396
+ "Hard wall-clock timeout for the codex subprocess in seconds "
397
+ f"(default {DEFAULT_TIMEOUT})."
398
+ ),
399
+ },
400
+ }
401
+
402
+ _CODEX_PROPS: dict[str, Any] = {
403
+ "prompt": _COMMON_PROPS["prompt"],
404
+ "cd": {
405
+ "type": "string",
406
+ "description": (
407
+ "Pass -C <DIR>: the working directory codex runs in. Defaults to this server's "
408
+ "own working directory."
409
+ ),
410
+ },
411
+ "model": _COMMON_PROPS["model"],
412
+ "config": _COMMON_PROPS["config"],
413
+ "sandbox": {
414
+ "type": "string",
415
+ "enum": list(_SANDBOX_MODES),
416
+ "description": (
417
+ "Pass --sandbox <MODE>: read-only, workspace-write or danger-full-access. "
418
+ "Overrides sandbox_mode from config.toml; network access still follows the "
419
+ "[sandbox_workspace_write] section there."
420
+ ),
421
+ },
422
+ "add_dir": {
423
+ "type": "array",
424
+ "items": {"type": "string"},
425
+ "description": (
426
+ "Extra writable directories. Each entry maps to a separate codex --add-dir flag "
427
+ "(repeatable, not comma-joined)."
428
+ ),
429
+ },
430
+ "profile": {
431
+ "type": "string",
432
+ "description": "Pass -p <PROFILE>: the config.toml profile to load.",
433
+ },
434
+ "ephemeral": _COMMON_PROPS["ephemeral"],
435
+ "skip_git_repo_check": _COMMON_PROPS["skip_git_repo_check"],
436
+ "extra_args": _COMMON_PROPS["extra_args"],
437
+ "env": _COMMON_PROPS["env"],
438
+ "timeout_seconds": _COMMON_PROPS["timeout_seconds"],
439
+ }
440
+
441
+ _REPLY_PROPS: dict[str, Any] = {
442
+ "thread_id": {
443
+ "type": "string",
444
+ "minLength": 1,
445
+ "description": (
446
+ "The thread to continue: the thread_id from a previous result's [codex] line. "
447
+ "Maps to codex exec resume <THREAD_ID>."
448
+ ),
449
+ },
450
+ "prompt": _COMMON_PROPS["prompt"],
451
+ "model": _COMMON_PROPS["model"],
452
+ "config": _COMMON_PROPS["config"],
453
+ "ephemeral": _COMMON_PROPS["ephemeral"],
454
+ "skip_git_repo_check": _COMMON_PROPS["skip_git_repo_check"],
455
+ "extra_args": _COMMON_PROPS["extra_args"],
456
+ "env": _COMMON_PROPS["env"],
457
+ "timeout_seconds": _COMMON_PROPS["timeout_seconds"],
458
+ }
459
+
460
+ _CODEX_TOOL = Tool(
461
+ name="codex",
462
+ description=(
463
+ "Run a single non-interactive Codex session (`codex exec`) with the given prompt and "
464
+ "return its final message. codex is an agentic coding assistant that reads and, "
465
+ "depending on the sandbox, edits files inside the working directory. Forward-compatible: "
466
+ "unknown CLI flags can be passed via `extra_args`. The codex binary path is configurable "
467
+ "via $CODEX_CMD."
468
+ ),
469
+ inputSchema={
470
+ "type": "object",
471
+ "properties": _CODEX_PROPS,
472
+ "required": ["prompt"],
473
+ },
474
+ )
475
+
476
+ _REPLY_TOOL = Tool(
477
+ name="codex-reply",
478
+ description=(
479
+ "Continue a previous Codex session (`codex exec resume <THREAD_ID>`) with a follow-up "
480
+ "prompt and return the new final message. Only the flags `codex exec resume` accepts "
481
+ "are exposed; the working directory and sandbox come from the current configuration "
482
+ "(this server's own flags and config.toml), not from the original session."
483
+ ),
484
+ inputSchema={
485
+ "type": "object",
486
+ "properties": _REPLY_PROPS,
487
+ "required": ["thread_id", "prompt"],
488
+ },
489
+ )
490
+
491
+ _BOOL_FIELDS = ("ephemeral", "skip_git_repo_check")
492
+ _STR_FIELDS = ("prompt", "thread_id", "cd", "model", "sandbox", "profile")
493
+ _STR_ARRAY_FIELDS = ("config", "add_dir", "extra_args")
494
+
495
+ # Parameter names the deprecated `codex mcp-server` used, mapped to what this server calls
496
+ # them. A client migrated from that server is the most likely source of an unknown key, and
497
+ # a working directory or thread id it passed under the old name must not be dropped on the
498
+ # floor: the call would run in the wrong directory or start a new thread without a word.
499
+ _RENAMED_FROM_MCP_SERVER = {
500
+ "cwd": "cd",
501
+ "threadId": "thread_id",
502
+ "conversationId": "thread_id",
503
+ }
504
+
505
+
506
+ def _validate_args(
507
+ args: dict[str, Any], *, required: tuple[str, ...], allowed: dict[str, Any]
508
+ ) -> str | None:
509
+ """Return the first schema violation, or None when the arguments are usable.
510
+
511
+ The low-level Server advertises `inputSchema` and never applies it, so this walks the
512
+ properties by hand. Checking only that the required keys are present is not enough:
513
+ every value here reaches argv.
514
+ """
515
+ for key in args:
516
+ if key in allowed:
517
+ continue
518
+ renamed = _RENAMED_FROM_MCP_SERVER.get(key)
519
+ if renamed in allowed:
520
+ return f"unknown parameter `{key}`; this server calls it `{renamed}`"
521
+ return f"unknown parameter `{key}`"
522
+ for key in required:
523
+ # `is None`, not `not in`: an explicit null passes a presence check but then reaches
524
+ # argv as the string "None".
525
+ if args.get(key) is None:
526
+ return f"`{key}` is required"
527
+ for key in _STR_FIELDS:
528
+ value = args.get(key)
529
+ if value is not None and not isinstance(value, str):
530
+ return f"`{key}` must be a string, got {type(value).__name__}"
531
+ for key in required:
532
+ if not args[key]:
533
+ return f"`{key}` must not be empty"
534
+ for key in _BOOL_FIELDS:
535
+ value = args.get(key)
536
+ # An exact type check rather than truthiness: `bool("false")` is True.
537
+ if value is not None and not isinstance(value, bool):
538
+ return f"`{key}` must be a boolean, got {type(value).__name__}"
539
+ for key in _STR_ARRAY_FIELDS:
540
+ value = args.get(key)
541
+ if value is None:
542
+ continue
543
+ # A bare string satisfies `for d in add_dir` and `list(extra_args)` but iterates
544
+ # character by character, so "docs" would expand into four --add-dir flags.
545
+ if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
546
+ return f"`{key}` must be an array of strings"
547
+ for item in args.get("config") or []:
548
+ # codex parses `-c key=value`; a bare key would be rejected there with a message
549
+ # that no longer names the parameter the client got wrong.
550
+ if "=" not in item:
551
+ return f"`config` entries must be key=value strings, got {item!r}"
552
+ sandbox = args.get("sandbox")
553
+ if sandbox is not None and sandbox not in _SANDBOX_MODES:
554
+ return f"`sandbox` must be one of {', '.join(_SANDBOX_MODES)}, got {sandbox!r}"
555
+ env = args.get("env")
556
+ if env is not None and (
557
+ not isinstance(env, dict)
558
+ or not all(isinstance(k, str) and isinstance(v, str) for k, v in env.items())
559
+ ):
560
+ return "`env` must be an object mapping strings to strings"
561
+ timeout = args.get("timeout_seconds")
562
+ if timeout is not None:
563
+ # bool subclasses int, so it has to be excluded before isinstance would accept it
564
+ if isinstance(timeout, bool) or not isinstance(timeout, int):
565
+ return f"`timeout_seconds` must be an integer, got {type(timeout).__name__}"
566
+ if not 30 <= timeout <= 3600:
567
+ return f"`timeout_seconds` must be between 30 and 3600, got {timeout}"
568
+ return None
569
+
570
+
571
+ def _error_result(text: str) -> CallToolResult:
572
+ return CallToolResult(content=[TextContent(type="text", text=f"[ERROR] {text}")], is_error=True)
573
+
574
+
575
+ async def _invoke(arguments: dict[str, Any] | None, *, reply: bool) -> CallToolResult:
576
+ """The body of both tools, taking a plain dict so callers need no SDK request object."""
577
+ args = dict(arguments or {})
578
+
579
+ # `codex exec resume` does not take `cd`, `sandbox`, `add_dir` or `profile`; they are
580
+ # absent from _REPLY_PROPS, so the unknown-key check refuses them by name, where codex's
581
+ # own error would only name the flag.
582
+ invalid = _validate_args(
583
+ args,
584
+ required=("thread_id", "prompt") if reply else ("prompt",),
585
+ allowed=_REPLY_PROPS if reply else _CODEX_PROPS,
586
+ )
587
+ if invalid is not None:
588
+ return _error_result(f"input validation: {invalid}")
589
+
590
+ argv = _build_argv(
591
+ thread_id=args.get("thread_id") if reply else None,
592
+ cd=args.get("cd"),
593
+ model=args.get("model"),
594
+ config=args.get("config"),
595
+ sandbox=args.get("sandbox"),
596
+ add_dir=args.get("add_dir"),
597
+ profile=args.get("profile"),
598
+ ephemeral=bool(args.get("ephemeral", False)),
599
+ skip_git_repo_check=bool(args.get("skip_git_repo_check", False)),
600
+ extra_args=args.get("extra_args"),
601
+ )
602
+
603
+ result = await _run_codex(
604
+ argv=argv,
605
+ prompt=args["prompt"],
606
+ timeout=int(args.get("timeout_seconds") or DEFAULT_TIMEOUT),
607
+ env_overrides=args.get("env"),
608
+ )
609
+ parsed = _parse_events(result.get("stdout", ""))
610
+ text = _format_result(result)
611
+ structured: dict[str, Any] | None = None
612
+ if parsed["thread_id"]:
613
+ # The same shape the deprecated `codex mcp-server` returned, so a client that reads
614
+ # structuredContent.threadId keeps working.
615
+ structured = {"threadId": parsed["thread_id"], "content": _response_text(parsed)}
616
+ return CallToolResult(
617
+ content=[TextContent(type="text", text=text)],
618
+ structured_content=structured,
619
+ is_error=_run_failed(result),
620
+ )
621
+
622
+
623
+ async def codex(arguments: dict[str, Any] | None) -> CallToolResult:
624
+ return await _invoke(arguments, reply=False)
625
+
626
+
627
+ async def codex_reply(arguments: dict[str, Any] | None) -> CallToolResult:
628
+ return await _invoke(arguments, reply=True)
629
+
630
+
631
+ async def on_list_tools(
632
+ ctx: ServerRequestContext, params: PaginatedRequestParams | None
633
+ ) -> ListToolsResult:
634
+ return ListToolsResult(tools=[_CODEX_TOOL, _REPLY_TOOL])
635
+
636
+
637
+ async def on_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
638
+ # An escaping exception would become a JSON-RPC error, which clients treat as a transport
639
+ # failure rather than as something to show the model. CancelledError derives from
640
+ # BaseException, so peer cancellation still propagates and _run_codex's killpg still runs.
641
+ try:
642
+ if params.name == "codex":
643
+ return await codex(params.arguments)
644
+ if params.name == "codex-reply":
645
+ return await codex_reply(params.arguments)
646
+ return _error_result(f"unknown tool: {params.name}")
647
+ except Exception as exc:
648
+ logger.exception("%s failed", params.name)
649
+ return _error_result(f"{type(exc).__name__}: {exc}")
650
+
651
+
652
+ server: Server = Server(
653
+ _PKG_NAME,
654
+ version=_pkg_version(_PKG_NAME),
655
+ on_list_tools=on_list_tools,
656
+ on_call_tool=on_call_tool,
657
+ )
658
+ # v2 attaches an OpenTelemetryMiddleware by default. It is a no-op without an exporter, but
659
+ # it puts a tracing layer on the path of every request and reads OTEL_* environment
660
+ # variables in-process. This file is meant to be the whole story, so the layer is dropped
661
+ # rather than left implicit; delete this line to get the SDK default back.
662
+ server.middleware.clear()
663
+
664
+
665
+ async def _amain() -> None:
666
+ logging.basicConfig(
667
+ level=os.environ.get("CODEX_CLI_MCP_SLIM_LOG_LEVEL", "INFO"),
668
+ format="[codex-cli-mcp-slim] %(levelname)s %(message)s",
669
+ )
670
+ if SERVER_ARGS:
671
+ logger.info("server-level codex exec flags: %s", SERVER_ARGS)
672
+ async with stdio_server() as (read, write):
673
+ await server.run(read, write, server.create_initialization_options())
674
+
675
+
676
+ _USAGE = """\
677
+ usage: codex-cli-mcp-slim [CODEX_EXEC_FLAGS...]
678
+
679
+ Start the MCP server on stdio. Every argument is placed right after `codex exec`
680
+ on every tool call (for example: -c model_reasoning_effort=high -C /srv/scratch).
681
+ """
682
+
683
+
684
+ def main() -> None:
685
+ # Everything on the server's own command line is handed to codex exec, verbatim, on
686
+ # every call. The two exceptions exist only so that a person who runs the command by
687
+ # hand to see what it is does not start a stdio server waiting on a terminal.
688
+ if sys.argv[1:] in (["--help"], ["-h"]):
689
+ print(_USAGE, end="")
690
+ return
691
+ if sys.argv[1:] == ["--version"]:
692
+ print(f"{_PKG_NAME} {_pkg_version(_PKG_NAME)}")
693
+ return
694
+ SERVER_ARGS[:] = sys.argv[1:]
695
+ asyncio.run(_amain())
696
+
697
+
698
+ if __name__ == "__main__":
699
+ main()
@@ -0,0 +1,354 @@
1
+ Metadata-Version: 2.4
2
+ Name: codex-cli-mcp-slim
3
+ Version: 0.1.0
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
+ Author-email: tksfjt1024 <tksfjt1024@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/tksfjt1024/codex-cli-mcp-slim
8
+ Project-URL: Repository, https://github.com/tksfjt1024/codex-cli-mcp-slim
9
+ Project-URL: Issues, https://github.com/tksfjt1024/codex-cli-mcp-slim/issues
10
+ Keywords: mcp,model-context-protocol,codex,codex-cli,openai,claude,anthropic,ai,llm,coding-agent
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: POSIX
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Utilities
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: mcp<3,>=2.0.0
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest>=8.0; extra == "test"
31
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
32
+ Provides-Extra: dev
33
+ Requires-Dist: ruff>=0.6; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # codex-cli-mcp-slim
37
+
38
+ A thin, auditable [MCP](https://modelcontextprotocol.io) server wrapping the [Codex CLI](https://github.com/openai/codex) (`codex exec`).
39
+
40
+ [![PyPI version](https://img.shields.io/pypi/v/codex-cli-mcp-slim.svg)](https://pypi.org/project/codex-cli-mcp-slim/)
41
+ [![Python versions](https://img.shields.io/pypi/pyversions/codex-cli-mcp-slim.svg)](https://pypi.org/project/codex-cli-mcp-slim/)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
43
+ [![CI](https://github.com/tksfjt1024/codex-cli-mcp-slim/actions/workflows/ci.yml/badge.svg)](https://github.com/tksfjt1024/codex-cli-mcp-slim/actions/workflows/ci.yml)
44
+
45
+ ## Why
46
+
47
+ `codex mcp-server`, the command that let other MCP clients call Codex, is
48
+ deprecated, and its removal has been merged upstream
49
+ ([openai/codex#42993](https://github.com/openai/codex/pull/42993)): releases up
50
+ to 0.153.x still ship it, later ones will not. Its replacement, the Codex app
51
+ server, speaks its own JSON-RPC protocol rather than MCP. This server keeps the old
52
+ integration point alive: it exposes the same two tools, `codex` and
53
+ `codex-reply`, and runs `codex exec` underneath. `codex exec` is the Codex CLI's
54
+ non-interactive mode: one prompt in, the agent works on its own, one final
55
+ message out.
56
+
57
+ When you add an MCP server to your AI coding tool, every prompt and code snippet
58
+ you send flows through that wrapper. Most CLI-wrapping MCP servers are small,
59
+ individually maintained packages, and recent supply-chain incidents
60
+ (`xz-utils`, `postmark-mcp`, the npm `chalk`/`debug` compromise) show that
61
+ "small and useful" is not the same as "safe to trust blindly."
62
+
63
+ This project takes the opposite stance: instead of asking you to trust it, it
64
+ tries to be **easy to audit**.
65
+
66
+ - **Single file** — the whole server is `src/codex_cli_mcp_slim/server.py`,
67
+ readable end-to-end in one sitting
68
+ - **One third-party dependency** (`mcp`) — minimal supply-chain surface
69
+ - **Faithful CLI mapping** — every typed parameter mirrors a real `codex exec`
70
+ flag by name, so it is obvious which flags an invocation actually sets
71
+ - **Prompt over stdin** — the prompt never appears in the process list and is
72
+ not bounded by the argv size limit
73
+ - **Forward-compatible** — any new or uncommon `codex exec` flag is reachable via
74
+ `extra_args` without touching this server
75
+ - **Configurable binary path** — `$CODEX_CMD` lets you swap or wrap the `codex`
76
+ binary
77
+ - **Transparent** — every invocation logs the exact argv to stderr
78
+
79
+ Read `server.py` before you install. That is the point.
80
+
81
+ ## Prerequisites
82
+
83
+ - The `codex` CLI installed and on `$PATH` (or pointed to via `$CODEX_CMD`). See
84
+ the [official Codex CLI repository](https://github.com/openai/codex). This server
85
+ always passes `--json` and reads the prompt from stdin (`codex exec -`), both of
86
+ which `codex exec` documents.
87
+ - `codex` already **authenticated** — this wrapper does not manage login; it
88
+ surfaces `codex`'s own error output if the CLI is not ready.
89
+
90
+ ## Installation
91
+
92
+ ```bash
93
+ # Run directly without installing
94
+ uvx codex-cli-mcp-slim
95
+
96
+ # Install from PyPI
97
+ pip install codex-cli-mcp-slim
98
+
99
+ # Run from GitHub HEAD
100
+ uvx --from git+https://github.com/tksfjt1024/codex-cli-mcp-slim codex-cli-mcp-slim
101
+ ```
102
+
103
+ ## Usage as an MCP server
104
+
105
+ ### Claude Code
106
+
107
+ ```bash
108
+ claude mcp add codex uvx codex-cli-mcp-slim
109
+ ```
110
+
111
+ Or manually in `~/.claude.json`:
112
+
113
+ ```json
114
+ {
115
+ "mcpServers": {
116
+ "codex": {
117
+ "type": "stdio",
118
+ "command": "uvx",
119
+ "args": ["codex-cli-mcp-slim"]
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ If `codex` is not on the launching process's `$PATH`, point `$CODEX_CMD` at it:
126
+
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "codex": {
131
+ "type": "stdio",
132
+ "command": "uvx",
133
+ "args": ["codex-cli-mcp-slim"],
134
+ "env": { "CODEX_CMD": "/absolute/path/to/codex" }
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### Replacing `codex mcp-server`
141
+
142
+ An entry that used to launch `codex mcp-server` keeps its server name and its
143
+ tool names; only `command` and `args` change. Before:
144
+
145
+ ```json
146
+ {
147
+ "mcpServers": {
148
+ "codex": {
149
+ "type": "stdio",
150
+ "command": "codex",
151
+ "args": ["mcp-server"]
152
+ }
153
+ }
154
+ }
155
+ ```
156
+
157
+ After:
158
+
159
+ ```json
160
+ {
161
+ "mcpServers": {
162
+ "codex": {
163
+ "type": "stdio",
164
+ "command": "uvx",
165
+ "args": ["codex-cli-mcp-slim"]
166
+ }
167
+ }
168
+ }
169
+ ```
170
+
171
+ Parameter names differ from the old server where `codex exec` names the flag
172
+ differently: `cwd` is now `cd` (the `-C/--cd` flag), and `codex-reply` takes
173
+ `thread_id` instead of `threadId`. The result's `structuredContent` field keeps
174
+ the shape the old server returned, `{"threadId": ..., "content": ...}`.
175
+
176
+ ### Other MCP clients
177
+
178
+ Any MCP-compatible client can launch the server via stdio:
179
+
180
+ ```bash
181
+ uvx codex-cli-mcp-slim
182
+ ```
183
+
184
+ ## Server-level flags
185
+
186
+ Everything on the server's own command line is placed right after `codex exec`
187
+ on every invocation. One MCP-client entry can therefore pin a reasoning effort, a
188
+ model or a working directory for all of its calls. Two entries that differ only
189
+ in reasoning effort look like this:
190
+
191
+ ```json
192
+ {
193
+ "mcpServers": {
194
+ "codex-medium": {
195
+ "type": "stdio",
196
+ "command": "uvx",
197
+ "args": ["codex-cli-mcp-slim", "-c", "model_reasoning_effort=medium"]
198
+ },
199
+ "codex-high": {
200
+ "type": "stdio",
201
+ "command": "uvx",
202
+ "args": ["codex-cli-mcp-slim", "-c", "model_reasoning_effort=high"]
203
+ }
204
+ }
205
+ }
206
+ ```
207
+
208
+ A call to `codex-high` runs
209
+ `codex exec -c model_reasoning_effort=high [per-call flags] --json -`. Per-call
210
+ flags come after the server-level ones, and `-c` may repeat with the last one
211
+ winning, so a per-call `config` entry overrides a server-level `-c`. Single-value
212
+ flags such as `-m` and `-C` may not repeat: `codex` rejects the second one, and
213
+ the tool result carries that error. Keep server-level flags and per-call
214
+ parameters disjoint for those.
215
+
216
+ ## Tool: `codex`
217
+
218
+ Runs a single non-interactive Codex session (`codex exec`). `codex` is an
219
+ agentic assistant: it reads and, depending on the sandbox, edits files in the
220
+ working directory to fulfil the request, then prints its final message.
221
+
222
+ The tool returns that final message followed by one metadata line:
223
+
224
+ ```
225
+ [codex] thread_id=019a2b3c-1d4e-7f60-8a9b-0c1d2e3f4a5b status=completed input_tokens=13894 cached_input_tokens=11904 output_tokens=612
226
+ ```
227
+
228
+ `thread_id` and `status` are always present; the token fields appear when the
229
+ run reported them. `isError` is the flag on an MCP tool result that tells the
230
+ client a call failed. This server sets it when `codex` exited non-zero, when the
231
+ subprocess timed out, and when the turn itself failed. The last case matters
232
+ because `codex exec` exits 0 after a failure inside the model API; the tool
233
+ result then carries the error text instead of coming back as a successful call:
234
+
235
+ ```
236
+ [ERROR] codex failed
237
+
238
+ returncode=0
239
+
240
+ errors:
241
+ Unsupported value: 'none' is not supported with the ... model.
242
+
243
+ [codex] thread_id=019a2b3c-... status=failed
244
+
245
+ argv: ['codex', 'exec', '--json', '-']
246
+ ```
247
+
248
+ Pass the `thread_id` to `codex-reply` to continue the same session.
249
+
250
+ | Parameter | Type | Description |
251
+ | --------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
252
+ | `prompt` (required) | string | Prompt sent verbatim to `codex` on stdin |
253
+ | `cd` | string | Pass `-C <DIR>`: the working directory; defaults to the server's own |
254
+ | `model` | string | Pass `-m <MODEL>` |
255
+ | `config` | string[] | `key=value` overrides; each maps to one `-c` (repeatable, last wins) |
256
+ | `sandbox` | string | Pass `--sandbox`: `read-only`, `workspace-write` or `danger-full-access`. See note below |
257
+ | `add_dir` | string[] | Extra writable directories; each maps to one `--add-dir` (repeatable, not comma-joined) |
258
+ | `profile` | string | Pass `-p <PROFILE>` |
259
+ | `ephemeral` | bool | Pass `--ephemeral` (do not write the session transcript codex keeps under `$CODEX_HOME/sessions`) |
260
+ | `skip_git_repo_check` | bool | Pass `--skip-git-repo-check` (allow a working directory outside a git repository) |
261
+ | `extra_args` | string[] | Raw CLI flags appended verbatim. Do not pass `--json` or a prompt; the server adds both |
262
+ | `env` | object | Extra environment variables for the `codex` subprocess |
263
+ | `timeout_seconds` | int | Hard wall-clock timeout for the subprocess, 30 to 3600 (default 1800) |
264
+
265
+ Unknown parameters are refused rather than ignored, so a call that still uses
266
+ the old server's `cwd` gets an error naming `cd` instead of running in the
267
+ wrong directory.
268
+
269
+ ### Security note: `sandbox`
270
+
271
+ `codex exec` reads its sandbox mode from its own configuration file
272
+ (`~/.codex/config.toml` by default) unless `--sandbox` is given.
273
+ `danger-full-access` removes the filesystem and network sandbox entirely;
274
+ `workspace-write` makes the working directory (and any `add_dir`) writable.
275
+ `--sandbox` overrides only the mode; whether `workspace-write` gets network
276
+ access still follows the `[sandbox_workspace_write]` section of `config.toml`.
277
+ The parameter mirrors the flag so that whichever mode a call runs
278
+ under is visible in the arguments and in the logged argv. This server does not
279
+ pass `--dangerously-bypass-approvals-and-sandbox`; reach it via `extra_args` if
280
+ you really mean it.
281
+
282
+ ## Tool: `codex-reply`
283
+
284
+ Continues a previous session (`codex exec resume <THREAD_ID>`) with a follow-up
285
+ prompt and returns the new final message. Only the flags `codex exec resume`
286
+ accepts are exposed, so `cd`, `sandbox`, `add_dir` and `profile` are refused
287
+ here. The working directory and sandbox of a reply come from the current
288
+ configuration, that is, the server-level flags and `config.toml`, not from the
289
+ original session.
290
+
291
+ | Parameter | Type | Description |
292
+ | ----------------------- | -------- | ----------------------------------------------------------------- |
293
+ | `thread_id` (required) | string | The `thread_id` from a previous result's `[codex]` line |
294
+ | `prompt` (required) | string | Follow-up prompt, sent on stdin |
295
+ | `model` | string | Pass `-m <MODEL>` |
296
+ | `config` | string[] | `key=value` overrides; each maps to one `-c` |
297
+ | `ephemeral` | bool | Pass `--ephemeral` |
298
+ | `skip_git_repo_check` | bool | Pass `--skip-git-repo-check` |
299
+ | `extra_args` | string[] | Raw CLI flags appended verbatim |
300
+ | `env` | object | Extra environment variables for the `codex` subprocess |
301
+ | `timeout_seconds` | int | Hard wall-clock timeout for the subprocess, 30 to 3600 (default 1800) |
302
+
303
+ ## Timeout configuration
304
+
305
+ `timeout_seconds` is this wrapper's hard wall-clock limit (default 1800, or
306
+ `$CODEX_CLI_MCP_SLIM_TIMEOUT`). On timeout, the wrapper kills the subprocess's
307
+ whole process group and then waits up to 20 additional seconds to collect any
308
+ buffered output and reap the process, so the effective ceiling is
309
+ `timeout_seconds + 20`. A timed-out call is flagged `isError` and carries
310
+ whatever `codex` had printed so far.
311
+
312
+ ## Forward-compatibility example
313
+
314
+ If a future `codex exec` release adds a new flag (say `--super-mode`), use it
315
+ immediately without updating this server:
316
+
317
+ ```jsonc
318
+ {
319
+ "name": "codex",
320
+ "arguments": {
321
+ "prompt": "...",
322
+ "extra_args": ["--super-mode"]
323
+ }
324
+ }
325
+ ```
326
+
327
+ ## Configuration
328
+
329
+ | Environment variable | Default | Purpose |
330
+ | ------------------------------ | ------- | ---------------------------------------- |
331
+ | `CODEX_CMD` | `codex` | Path to the `codex` CLI binary |
332
+ | `CODEX_CLI_MCP_SLIM_TIMEOUT` | `1800` | Default subprocess timeout in seconds |
333
+ | `CODEX_CLI_MCP_SLIM_LOG_LEVEL` | `INFO` | Logging level for stderr diagnostics |
334
+
335
+ `codex` itself reads its configuration file and credentials from `$CODEX_HOME`
336
+ (`~/.codex` by default), so an MCP-client entry can point a server at a
337
+ dedicated configuration directory through its `env` block.
338
+
339
+ ## Development
340
+
341
+ ```bash
342
+ # Install dev dependencies
343
+ pip install -e ".[test,dev]"
344
+
345
+ # Lint
346
+ ruff check .
347
+
348
+ # Test
349
+ pytest
350
+ ```
351
+
352
+ ## License
353
+
354
+ [MIT](./LICENSE) © tksfjt1024
@@ -0,0 +1,9 @@
1
+ codex_cli_mcp_slim/__init__.py,sha256=hbKd2iJtOvMLYuoawTIrykBsvv5VQS7IgKRgT4mQSUw,257
2
+ codex_cli_mcp_slim/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ codex_cli_mcp_slim/server.py,sha256=T7oApKG8LCAshQfZHC8ESgceI2fn-rETjJwNwTmTw2M,28240
4
+ codex_cli_mcp_slim-0.1.0.dist-info/licenses/LICENSE,sha256=Tr5DgZN5ASK_iNxUlQLjINkMg2zUJZuSh3JEStppNV4,1067
5
+ codex_cli_mcp_slim-0.1.0.dist-info/METADATA,sha256=nrJ9SWmVOBfOgHH7TcG_8iqbAUm16iyEVLG5Sw2dj8M,14605
6
+ codex_cli_mcp_slim-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
7
+ codex_cli_mcp_slim-0.1.0.dist-info/entry_points.txt,sha256=ELoQ0plLqqlCAMRqQniZO31woExkAJYBmKvPcbFbMMo,70
8
+ codex_cli_mcp_slim-0.1.0.dist-info/top_level.txt,sha256=ek9G3GaWoULhzBhjNfP4C5avRGJ2dh67F4xEG2KQ9ts,19
9
+ codex_cli_mcp_slim-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ codex-cli-mcp-slim = codex_cli_mcp_slim.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tksfjt1024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ codex_cli_mcp_slim