oct
Loading...
Searching...
No Matches
__main__.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oct/mcp/__main__.py
4
5"""
6Purpose
7-------
8Entry point for ``python -m oct.mcp``: declare the command-line surface
9of the ``oct-mcp`` server, parse it, and hand the resolved options to
10:func:`oct.mcp.server.start_server`.
11
12Responsibilities
13----------------
14- Declare every CLI option (project root, profile, config file,
15 transport, bind host and port, metrics port) in one parser.
16- Translate ``--config`` into a :class:`~pathlib.Path` before handing it
17 on, so the server layer never parses strings.
18- Under the ``stdio`` transport, divert standard output (fd 1) to
19 standard error from just after argument parsing until
20 :func:`oct.mcp.server.start_server` hands it to the SDK transport
21 (:mod:`oct.mcp.stdout_diversion`), so nothing written during startup
22 reaches the JSON-RPC channel.
23- Translate an interactive ``Ctrl-C`` into a clean exit code 0.
24
25Usage
26-----
27 python -m oct.mcp [--project-root PATH] [--profile PROFILE]
28 [--transport {stdio,sse,streamable-http}]
29 [--host HOST] [--port PORT]
30 [--metrics-port PORT]
31
32Options
33-------
34--project-root PATH
35 Absolute or relative path to the Option C project root.
36 Defaults to auto-discovery from the current working directory.
37
38--profile PROFILE
39 MCP security profile: ``default``, ``strict``, or ``airgapped``.
40 Overrides ``OCT_MCP_PROFILE`` and the config file value.
41
42--config PATH
43 Path to the user-level config file.
44 Defaults to ``~/.oct-mcp/config.json``.
45
46--transport {stdio,sse,streamable-http}
47 MCP transport. ``stdio`` (default) for local AI host integration
48 (Claude Code, Cursor, VS Code). ``streamable-http`` for remote
49 access. ``sse`` is the legacy remote transport, kept for hosts that
50 have not moved to streamable HTTP.
51
52--host HOST
53 Bind host for the HTTP transports (default: ``127.0.0.1``).
54 Only used with ``--transport streamable-http`` or ``--transport
55 sse``. Use ``0.0.0.0`` with caution and only behind a reverse proxy
56 with TLS and authentication.
57
58--port PORT
59 Bind port for the HTTP transports (default: ``3001``).
60 Only used with ``--transport streamable-http`` or ``--transport
61 sse``.
62
63--metrics-port PORT
64 Start a Prometheus ``/metrics`` HTTP server on this port.
65 Disabled by default. Requires ``oct[metrics]`` to be installed.
66
67Environment variables
68---------------------
69OCT_MCP_SAFE_MODE=1 Block all tool calls (emergency kill-switch).
70OCT_TRUST_REPO_CONFIG=1 Trust project-level config (debug_config.json).
71OCT_MCP_PROFILE=<name> Override active profile.
72OCT_MCP_SANDBOX=<name> Override sandbox backend (auto/native/bubblewrap/sandbox_exec).
73
74Diagnostics
75-----------
76Domain: MCP
77Levels:
78 L1 - errors: startup failures raised out of :func:`start_server`
79 L2 - lifecycle: server start and shutdown
80 L3 - request details
81 L4 - deep trace
82
83This module emits one ``_dbg`` record of its own, the L3 server entry,
84after argument parsing and after the stdio diversion has started, so it
85can never reach the protocol channel. Every other diagnostic for a run
86comes from :mod:`oct.mcp.server` and the pipeline layers beneath it.
87
88Contracts
89---------
90- :func:`main` returns ``None``. A fatal startup error reaches the
91 caller as the ``SystemExit(1)`` raised by
92 :func:`oct.mcp.server.start_server`, so the process exit code is the
93 server layer's, not this module's.
94- An unrecognised ``--transport`` or ``--profile`` value is refused by
95 ``argparse`` with exit code 2 and the offending value named.
96- ``KeyboardInterrupt`` exits 0: stopping a stdio server with Ctrl-C is
97 the normal way a host detaches, not a failure.
98- Under the ``stdio`` transport, fd 1 is diverted before the first
99 ``_dbg`` call. :func:`oct.mcp.server.start_server` hands it back
100 immediately before the transport runs, and exits with code 1 instead
101 of serving when that fails; :func:`main` hands it back after a
102 startup failure or a ``Ctrl-C``. ``--help`` and ``argparse`` errors
103 are printed before the diversion, to standard output and standard
104 error as before. The HTTP transports are not diverted.
105- Nothing imported before the diversion starts (the ``oct.mcp``
106 package and this module's module-level imports) may emit a ``_dbg``
107 record at import time, because such a record would reach the wire.
108 :mod:`oct.mcp.server` is imported lazily, after the diversion. The
109 end-to-end test with every domain at level 9 in
110 ``tests/tests_mcp/test_mcp_stdout_diversion.py`` guards this.
111
112Dependencies
113------------
114- oct.core.diagnostics (_dbg structured logger)
115- oct.mcp.stdout_diversion (divert_stdout and restore_stdout, which keep startup output off the stdio wire)
116- oct.mcp.server (lazy; start_server, which runs the server with the parsed options)
117"""
118
119from __future__ import annotations
120
121import argparse
122import sys
123from pathlib import Path
124from oct.core.diagnostics import _dbg
125from oct.mcp.stdout_diversion import divert_stdout, restore_stdout
126
127
128def _build_parser() -> argparse.ArgumentParser:
129 """Build the ``python -m oct.mcp`` argument parser.
130
131 Factored out of :func:`_parse_args` so the declared transport
132 choices can be inspected without parsing an argument vector.
133 """
134 parser = argparse.ArgumentParser(
135 prog="python -m oct.mcp",
136 description="Option C Tools MCP server.",
137 )
138 parser.add_argument(
139 "--project-root",
140 metavar="PATH",
141 default=None,
142 help="Project root directory (default: auto-discover from cwd).",
143 )
144 parser.add_argument(
145 "--profile",
146 choices=["default", "strict", "airgapped"],
147 default=None,
148 help="MCP security profile (default: from config or 'default').",
149 )
150 parser.add_argument(
151 "--config",
152 metavar="PATH",
153 default=None,
154 help="Path to ~/.oct-mcp/config.json (default: standard location).",
155 )
156 parser.add_argument(
157 "--transport",
158 choices=["stdio", "sse", "streamable-http"],
159 default="stdio",
160 help=(
161 # OCT-LINT: disable=no_hardcoded_secrets reason="argparse help prose shown to the operator, not a credential"
162 "MCP transport: 'stdio' (default) for local AI host integration, "
163 "'streamable-http' for remote access, 'sse' for the legacy "
164 "HTTP/SSE remote transport."
165 ),
166 )
167 parser.add_argument(
168 "--host",
169 default="127.0.0.1",
170 help=(
171 "Bind host for the HTTP transports (default: 127.0.0.1). "
172 "Only used with --transport streamable-http or --transport sse."
173 ),
174 )
175 parser.add_argument(
176 "--port",
177 type=int,
178 default=3001,
179 help=(
180 "Bind port for the HTTP transports (default: 3001). Only used "
181 "with --transport streamable-http or --transport sse."
182 ),
183 )
184 parser.add_argument(
185 "--metrics-port",
186 type=int,
187 default=None,
188 metavar="PORT",
189 help=(
190 "Start a Prometheus /metrics HTTP server on this port. "
191 "Disabled by default. Requires oct[metrics]."
192 ),
193 )
194 return parser
195
196
197def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
198 """Parse *argv* with the :func:`_build_parser` parser."""
199 return _build_parser().parse_args(argv)
200
201
202def main(argv: list[str] | None = None) -> None:
203 """Parse CLI arguments and start the MCP server.
204
205 Under the ``stdio`` transport, standard output is diverted to
206 standard error right after parsing, before the first ``_dbg`` call,
207 and :func:`oct.mcp.server.start_server` hands it back to the SDK
208 transport immediately before serving (or exits with code 1 when it
209 cannot). The ``finally`` clause hands it back after a startup
210 failure too; after a normal run it is a no-op.
211 """
212 func = "main"
213 args = _parse_args(argv)
214 config_path = Path(args.config) if args.config else None
215
216 try:
217 if args.transport == "stdio":
218 divert_stdout()
219 _dbg("MCP", func, f"oct-mcp server entry transport={args.transport}", 3)
220 from oct.mcp.server import start_server
221 start_server(
222 project_root=args.project_root,
223 profile=args.profile,
224 config_path=config_path,
225 transport=args.transport,
226 host=args.host,
227 port=args.port,
228 metrics_port=args.metrics_port,
229 )
230 except KeyboardInterrupt:
231 sys.exit(0)
232 finally:
233 restore_stdout()
234
235
236if __name__ == "__main__":
237 main()
None main(list[str]|None argv=None)
Definition __main__.py:202
argparse.Namespace _parse_args(list[str]|None argv=None)
Definition __main__.py:197
argparse.ArgumentParser _build_parser()
Definition __main__.py:128