576df2cddd
Files changed: - .gitea/workflows/ci.yml - CHANGES.md - README.md - VERSION - instructions/mcp-read-server.md - tools/CONTRACT.md - tools/README.md - tools/chemenu/api.py - tools/chemenu/commands/doctor.py - tools/chemenu/commands/lint.py - tools/chemenu/commands/search.py - tools/chemenu/commands/types_cmd.py - tools/chemenu/config.py - tools/chemenu/corpus_cache.py - tools/chemenu/errors.py - tools/chemenu/frontmatter_io.py - tools/chemenu/lint_core.py - tools/chemenu/mcp/__init__.py - tools/chemenu/mcp/__main__.py - tools/chemenu/mcp/server.py - tools/chemenu/page.py - tools/chemenu/search/filters.py - tools/chemenu/search/registry.py - tools/chemenu/search/ripgrep.py - tools/chemenu/search/service.py - tools/chemenu/tests/conftest.py - tools/chemenu/tests/test_api.py - tools/chemenu/tests/test_corpus_cache.py - tools/chemenu/tests/test_doctor.py - tools/chemenu/tests/test_frontmatter_io.py - tools/chemenu/tests/test_instructions_cmd.py - tools/chemenu/tests/test_mcp_server.py - tools/chemenu/tests/test_new_page.py - tools/chemenu/tests/test_search.py - tools/chemenu/type_resolver.py - tools/chemenu/types_core.py - tools/requirements-mcp.txt
80 lines
2.9 KiB
Python
80 lines
2.9 KiB
Python
"""`python -m chemenu.mcp` - start the read server.
|
|
|
|
Deliberately argparse and not typer. This process is the one place that must
|
|
not pull the CLI head in: the whole point of the library boundary is that a
|
|
second consumer costs `yaml`, `jsonschema` and the MCP SDK, and nothing else.
|
|
|
|
python -m chemenu.mcp # stdio
|
|
python -m chemenu.mcp --transport streamable-http # behind the proxy
|
|
CHEMENU_ROOT=/srv/wiki python -m chemenu.mcp --transport streamable-http \
|
|
--host 0.0.0.0 --port 8000
|
|
|
|
`--host`/`--port` apply to `streamable-http` only. They are here because the
|
|
default binds loopback, and a server in a container with a reverse proxy in
|
|
front of it has to bind an interface the proxy can reach - that is a property
|
|
of the software, not of one installation. *Which* host and port a given
|
|
deployment picks is infrastructure and stays out of this repository.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
|
|
from chemenu.mcp.server import TRANSPORTS, TraceWouldWriteIntoCorpus, serve
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
prog="python -m chemenu.mcp",
|
|
description="Serve a Chemenu wiki read-only over MCP.",
|
|
)
|
|
parser.add_argument(
|
|
"--transport",
|
|
choices=TRANSPORTS,
|
|
default="stdio",
|
|
help="stdio for local use and testing; streamable-http for a deployed "
|
|
"instance behind the Traefik middleware (default: stdio)",
|
|
)
|
|
parser.add_argument(
|
|
"--root",
|
|
default=None,
|
|
help="The corpus to serve. Defaults to $CHEMENU_ROOT, then the checkout "
|
|
"this package lives in.",
|
|
)
|
|
parser.add_argument(
|
|
"--host",
|
|
default="127.0.0.1",
|
|
help="Interface to bind, streamable-http only. The loopback default is "
|
|
"deliberate; a container behind a reverse proxy needs 0.0.0.0.",
|
|
)
|
|
parser.add_argument(
|
|
"--port",
|
|
type=int,
|
|
default=8000,
|
|
help="Port to bind, streamable-http only (default: 8000)",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
|
|
# Passed only for the transport they mean anything to: `run_stdio_async`
|
|
# takes no host or port, and handing it one is a TypeError rather than a
|
|
# harmless no-op.
|
|
bind = (
|
|
{"host": args.host, "port": args.port}
|
|
if args.transport == "streamable-http"
|
|
else {}
|
|
)
|
|
|
|
try:
|
|
serve(transport=args.transport, root=args.root, **bind)
|
|
except TraceWouldWriteIntoCorpus as exc:
|
|
# Refused before binding anything: the message names both fixes, and a
|
|
# server that silently relocated the operator's telemetry instead would
|
|
# be a surprise buried in a log.
|
|
print(f"ERROR {exc}", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover - process entry point
|
|
raise SystemExit(main())
|