|
7 | 7 |
|
8 | 8 | from __future__ import annotations |
9 | 9 |
|
| 10 | +import argparse |
10 | 11 | import atexit |
11 | 12 | import inspect |
12 | 13 | import io |
13 | 14 | import sys |
14 | | -from typing import Any, Optional, TextIO |
| 15 | +from typing import Any, Optional, Sequence, TextIO |
15 | 16 |
|
16 | 17 | from mcp.server import MCPServer |
17 | 18 | from mcp.types import ToolAnnotations |
|
23 | 24 | from osw.service.context import Context, Policy |
24 | 25 | from osw.service.registry import Operation, bind, iter_operations |
25 | 26 | from osw.service.streams import force_utf8 |
| 27 | +from osw.service.version import version_line |
26 | 28 |
|
27 | 29 | INSTRUCTIONS = """\ |
28 | 30 | This server is pinned to exactly one OpenSemanticLab (OSL) instance for its |
@@ -114,9 +116,11 @@ def _build_server(report: Optional[TextIO] = None) -> tuple[MCPServer, Context]: |
114 | 116 | discarded. |
115 | 117 | """ |
116 | 118 | # Set before any shared-code logging runs, so every "[xxx] ..." message |
117 | | - # and wiki edit comment from shared code names this adapter. Also set as |
118 | | - # the first statement of main(), since main() prints on a start failure |
119 | | - # and this function is also callable on its own, e.g. from tests. |
| 119 | + # and wiki edit comment from shared code names this adapter. main() sets |
| 120 | + # it again before calling this function -- not as its first statement: |
| 121 | + # force_utf8, argument parsing and the --version branch run before it |
| 122 | + # there -- since main() prints on a start failure and this function is |
| 123 | + # also callable on its own, e.g. from tests. |
120 | 124 | config.set_log_prefix("osw-mcp") |
121 | 125 | # Before get_settings(), so a misconfiguration that makes loading raise |
122 | 126 | # still reports which files were read. Into `report` rather than stderr: |
@@ -155,20 +159,74 @@ def create_server() -> MCPServer: |
155 | 159 | return mcp |
156 | 160 |
|
157 | 161 |
|
158 | | -def main() -> None: |
159 | | - """Console-script entry point: build the server and serve over stdio.""" |
160 | | - # Before any write below. An MCP client starts this server with stderr on |
161 | | - # a pipe, so Python encodes it with the locale encoding, cp1252 on a |
162 | | - # German Windows system. The report holds the credential file path and the |
163 | | - # env file path, so a directory named "Muller" with an umlaut is enough to |
164 | | - # reach the client's log mangled. Reconfiguring in place also covers osw's |
165 | | - # own log handler, which holds this same stream object. |
| 162 | +# Wrapped by hand, below 80 columns, with the URL on a line of its own. |
| 163 | +# argparse's default formatter re-wraps a description with textwrap |
| 164 | +# (break_on_hyphens=True), so at some terminal widths a line ends with |
| 165 | +# ".../osw-" and the next starts with "python/blob/...", splitting the URL. |
| 166 | +# RawDescriptionHelpFormatter (below) prints this text verbatim instead. |
| 167 | +_DESCRIPTION = ( |
| 168 | + "Runs an MCP server for one OpenSemanticLab instance over stdio. An MCP\n" |
| 169 | + "client starts it, not a person from a shell. Configuration comes from\n" |
| 170 | + "environment variables, a .env file, or a credential file; see\n" |
| 171 | + "https://github.com/OpenSemanticLab/osw-python/blob/main/docs/tools/mcp.md." |
| 172 | +) |
| 173 | + |
| 174 | + |
| 175 | +def _build_parser() -> argparse.ArgumentParser: |
| 176 | + """Build the argument parser for the ``osw-mcp`` console script. |
| 177 | +
|
| 178 | + Parsing happens before :func:`_build_server`, so ``-h``, ``-V`` and an |
| 179 | + unrecognized argument never need credentials. |
| 180 | + """ |
| 181 | + parser = argparse.ArgumentParser( |
| 182 | + prog="osw-mcp", |
| 183 | + description=_DESCRIPTION, |
| 184 | + formatter_class=argparse.RawDescriptionHelpFormatter, |
| 185 | + # argparse accepts an unambiguous abbreviation by default (e.g. |
| 186 | + # "--vers"); osw's own CLI (click) does not, so this is turned off to |
| 187 | + # match: an argument that is not exactly one of the two below is |
| 188 | + # rejected. |
| 189 | + allow_abbrev=False, |
| 190 | + ) |
| 191 | + parser.add_argument( |
| 192 | + "-V", |
| 193 | + "--version", |
| 194 | + action="store_true", |
| 195 | + help="show the version and exit", |
| 196 | + ) |
| 197 | + return parser |
| 198 | + |
| 199 | + |
| 200 | +def main(argv: Optional[Sequence[str]] = None) -> None: |
| 201 | + """Console-script entry point: build the server and serve over stdio. |
| 202 | +
|
| 203 | + ``argv`` follows ``sys.argv[1:]``'s convention: ``None`` (the default) |
| 204 | + reads the real command line, and a caller that wants to pass its own, |
| 205 | + such as a test or ``python -m osw.mcp``, gives an explicit list instead. |
| 206 | + """ |
| 207 | + # Before any write below, including the usage error argparse prints for |
| 208 | + # an unrecognized argument, which echoes it back. An MCP client starts |
| 209 | + # this server with stderr on a pipe, so Python encodes it with the locale |
| 210 | + # encoding, cp1252 on a German Windows system. The report holds the |
| 211 | + # credential file path and the env file path, so a directory named |
| 212 | + # "Muller" with an umlaut is enough to reach the client's log mangled. |
| 213 | + # Reconfiguring in place also covers osw's own log handler, which holds |
| 214 | + # this same stream object. |
166 | 215 | # |
167 | 216 | # stdout is deliberately left alone. The SDK's stdio_server re-wraps the |
168 | 217 | # binary buffer as UTF-8 itself, and claims file descriptor 1 while doing |
169 | 218 | # it, so the JSON-RPC channel does not depend on this and changing it here |
170 | 219 | # would only add a way to interfere. |
171 | 220 | force_utf8(sys.stderr) |
| 221 | + args = _build_parser().parse_args(argv) |
| 222 | + |
| 223 | + if args.version: |
| 224 | + # No server runs on this path, so the reason above for leaving |
| 225 | + # stdout alone does not apply. |
| 226 | + force_utf8(sys.stdout) |
| 227 | + print(version_line("osw-mcp")) |
| 228 | + return |
| 229 | + |
172 | 230 | # See _build_server for why this is set here too. |
173 | 231 | config.set_log_prefix("osw-mcp") |
174 | 232 | report = io.StringIO() |
|
0 commit comments