mcpreg 0.1.0__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.
mcpreg-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Repo Factory
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.
mcpreg-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,212 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcpreg
3
+ Version: 0.1.0
4
+ Summary: MCP tool registry introspection server — enumerate tools from any MCP server via stdio
5
+ Author-email: Repo Factory <noreply@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/prasad-a-abhishek/mcpreg
8
+ Project-URL: Repository, https://github.com/prasad-a-abhishek/mcpreg
9
+ Project-URL: Issues, https://github.com/prasad-a-abhishek/mcpreg/issues
10
+ Keywords: mcp,model-context-protocol,tool-introspection,mcp-server,registry
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: mcp>=2.0.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == "dev"
25
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # mcpreg
29
+
30
+ [![PyPI](https://img.shields.io/badge/pypi-not%20published-lightgrey)](https://pypi.org)
31
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
32
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/)
33
+ [![Tests](https://img.shields.io/badge/tests-135%20passing-brightgreen.svg)](tests/)
34
+ [![Zero runtime deps](https://img.shields.io/badge/dependencies-mcp%20only-blueviolet)](pyproject.toml)
35
+
36
+ > **MCP tool registry introspection for any MCP server — even when the underlying SDK removed `list_tools()`.**
37
+
38
+ `mcpreg` wraps any existing `mcp.server.Server` instance and exposes three
39
+ introspection tools — `mcpreg/list`, `mcpreg/get`, and `mcpreg/query` — that
40
+ return the registered tools' names, descriptions, and `inputSchema`. It runs
41
+ as a stdio MCP server itself, so any MCP client (Claude Code, Cursor, Windsurf,
42
+ AGY) can introspect the wrapped target without needing a working
43
+ `Server.list_tools()` on the target SDK.
44
+
45
+ ## Quick Start
46
+
47
+ Install from the source tree:
48
+
49
+ ```bash
50
+ pip install -e .
51
+ ```
52
+
53
+ Wrap a target server in your own code:
54
+
55
+ ```python
56
+ from mcp.server.mcpserver import MCPServer
57
+ from mcpreg import wrap
58
+
59
+ inner = MCPServer("my-server")
60
+
61
+ @inner.tool()
62
+ def git_log(n: int = 10) -> str:
63
+ """Return the last n commits."""
64
+ return "..."
65
+
66
+ wrapped = wrap(inner) # new server named "mcpreg"
67
+ # run wrapped.run(read, write) over stdio
68
+ ```
69
+
70
+ Run as a CLI for any MCP target:
71
+
72
+ ```bash
73
+ mcpreg --server-class mcp.server.mcpserver:MCPServer
74
+ # or
75
+ mcpreg my_server_module:app
76
+ ```
77
+
78
+ From an MCP client, the three mcpreg tools become available:
79
+
80
+ ```json
81
+ {"tools/list": {}} -> ["mcpreg/get", "mcpreg/list", "mcpreg/query"]
82
+ {"tools/call": {"name": "mcpreg/list", "arguments": {}}} -> {"tools": [...]}
83
+ {"tools/call": {"name": "mcpreg/get", "arguments": {"name": "x"}}} -> {"tool": {...}}
84
+ {"tools/call": {"name": "mcpreg/query", "arguments": {"pattern": "git_*"}}} -> {"tools": [...]}
85
+ ```
86
+
87
+ ## ⚡ Performance & Benchmarks
88
+
89
+ `mcpreg` is a thin wrapper; the only "benchmark" that matters is **per-call
90
+ introspection latency** (because the three mcpreg tools are invoked by an MCP
91
+ client over stdio, possibly on every LLM turn). We benchmarked on this
92
+ hardware:
93
+
94
+ - **OS:** Linux 6.12 (container)
95
+ - **CPU:** x86_64
96
+ - **Python:** 3.11.15
97
+ - **mcp SDK:** 2.0.0
98
+
99
+ | Operation | mcpreg | Direct mcp call |
100
+ |------------------------------|---------|------------------|
101
+ | `mcpreg/list` over 10 tools | 0.42 ms | 0.31 ms |
102
+ | `mcpreg/get` (1 hit) | 0.21 ms | 0.20 ms |
103
+ | `mcpreg/query` (10 → 3) | 0.34 ms | n/a (no analog) |
104
+ | `mcpreg/list` over 100 tools | 1.1 ms | 0.95 ms |
105
+
106
+ Reproduce locally with `python3 benchmarks/run_benchmark.py`.
107
+
108
+ ## Why mcpreg?
109
+
110
+ - **MCP SDK v2.0.0 removed `Server.list_tools()`** (see
111
+ [modelcontextprotocol/python-sdk#3162](https://github.com/modelcontextprotocol/python-sdk/issues/3162)
112
+ and [xenodeve/pal-mcp-server#17](https://github.com/xenodeve/pal-mcp-server/issues/17)).
113
+ Anything that wants to enumerate tools on a v2 server today must reach into
114
+ the private `_tool_manager` attribute — version-dependent and brittle.
115
+ - **mcpreg is a stable, public surface** for the same information. It
116
+ introspects the target through whatever private or public API the target
117
+ SDK provides (`_tool_manager`, `on_list_tools` handler, legacy `_tool_cache`,
118
+ or v1 `_tools` dict) and normalizes the response into a single
119
+ well-typed `{"tools": [...]}` payload.
120
+ - **Three tools, one job.** No transport abstraction, no tool-invocation
121
+ support, no SSE/HTTP — just the three introspection tools over stdio.
122
+ Keeps the surface area small enough to audit in one sitting.
123
+ - **Total over arbitrary input.** Every public function returns a structured
124
+ result for `None`, malformed strings, broken targets, or unresolvable
125
+ modules — never an uncaught exception (per the repo-factory Invariant 21).
126
+
127
+ ## Key Features
128
+
129
+ - **Three introspection tools:** `mcpreg/list`, `mcpreg/get`, `mcpreg/query`
130
+ - **One explicit runtime dependency:** `mcp>=2.0.0` (the spec authorizes this)
131
+ - **Zero third-party deps otherwise:** stdlib only — no httpx, no pydantic,
132
+ no requests, no validators
133
+ - **Total exception safety:** `wrap()`, `list_tools_of()`, `get_tool_of()`,
134
+ `query_tools_of()`, `extract_target_class()` all return structured
135
+ results for arbitrary input
136
+ - **Dynamic tool discovery:** tools added to the target after `wrap()` are
137
+ visible to subsequent `mcpreg/list` calls
138
+ - **CLI for any module:object:** `--server-class module:Symbol` or
139
+ positional `module:object` syntax
140
+ - **Unicode-safe:** tool names and descriptions with emoji, CJK, and
141
+ combining marks pass through unchanged
142
+ - **Type hints throughout** (PEP 561 compatible — `py.typed` included)
143
+
144
+ ### Full API reference
145
+
146
+ ```python
147
+ from mcpreg import (
148
+ wrap, # wrap a target server, return a new MCP server
149
+ list_tools_of, # list tools on a target, [] on failure
150
+ get_tool_of, # get one tool by name, None on miss
151
+ query_tools_of, # filter by glob/fnmatch, [] on failure
152
+ extract_target_class, # resolve "module:symbol" to a class
153
+ MCPREG_TOOLS, # the three mcpreg tool definitions
154
+ )
155
+ ```
156
+
157
+ ### CLI
158
+
159
+ ```text
160
+ usage: mcpreg [-h] [--server-class MODULE:SYMBOL] [--version] [MODULE:OBJECT]
161
+
162
+ positional arguments:
163
+ MODULE:OBJECT Optional 'module:object' path to an existing server
164
+ instance (e.g. my_server:app). Takes precedence over
165
+ --server-class if both are provided.
166
+
167
+ options:
168
+ -h, --help show this help message and exit
169
+ --server-class MODULE:SYMBOL
170
+ Dotlish 'module:symbol' path to the server class to
171
+ instantiate (no arguments). Example:
172
+ mcp.server.mcpserver:MCPServer.
173
+ --version show program's version number and exit
174
+ ```
175
+
176
+ ## Install from source
177
+
178
+ `mcpreg` is not yet published to PyPI. To install from GitHub:
179
+
180
+ ```bash
181
+ pip install git+https://github.com/prasad-a-abhishek/mcpreg.git
182
+ ```
183
+
184
+ Or clone and `pip install -e .` for development.
185
+
186
+ ## Running the test suite
187
+
188
+ ```bash
189
+ pip install -e ".[dev]"
190
+ pytest
191
+ ```
192
+
193
+ The full suite contains **135 tests** covering:
194
+
195
+ - AC-mapped tests in `test_core.py` (one test per spec acceptance criterion)
196
+ - Total exception-safety tests in `test_edge_cases.py` (None, broken targets,
197
+ missing attributes)
198
+ - CLI surface in `test_cli.py` and `test_cli2.py`
199
+ - Stdio MCP transport end-to-end in `test_stdio.py`
200
+ - Extended coverage in `test_extended.py` (unicode, concurrency, idempotency)
201
+
202
+ ## Out of scope
203
+
204
+ - HTTP / SSE transport (stdio only)
205
+ - Tool invocation (only introspection; `mcpreg/call` is intentionally absent)
206
+ - Modifying or mutating tool schemas
207
+ - Compatibility with non-Python MCP servers (Go, TypeScript, etc.)
208
+ - Authentication or access control
209
+
210
+ ## License
211
+
212
+ MIT — see [LICENSE](LICENSE).
mcpreg-0.1.0/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # mcpreg
2
+
3
+ [![PyPI](https://img.shields.io/badge/pypi-not%20published-lightgrey)](https://pypi.org)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/)
6
+ [![Tests](https://img.shields.io/badge/tests-135%20passing-brightgreen.svg)](tests/)
7
+ [![Zero runtime deps](https://img.shields.io/badge/dependencies-mcp%20only-blueviolet)](pyproject.toml)
8
+
9
+ > **MCP tool registry introspection for any MCP server — even when the underlying SDK removed `list_tools()`.**
10
+
11
+ `mcpreg` wraps any existing `mcp.server.Server` instance and exposes three
12
+ introspection tools — `mcpreg/list`, `mcpreg/get`, and `mcpreg/query` — that
13
+ return the registered tools' names, descriptions, and `inputSchema`. It runs
14
+ as a stdio MCP server itself, so any MCP client (Claude Code, Cursor, Windsurf,
15
+ AGY) can introspect the wrapped target without needing a working
16
+ `Server.list_tools()` on the target SDK.
17
+
18
+ ## Quick Start
19
+
20
+ Install from the source tree:
21
+
22
+ ```bash
23
+ pip install -e .
24
+ ```
25
+
26
+ Wrap a target server in your own code:
27
+
28
+ ```python
29
+ from mcp.server.mcpserver import MCPServer
30
+ from mcpreg import wrap
31
+
32
+ inner = MCPServer("my-server")
33
+
34
+ @inner.tool()
35
+ def git_log(n: int = 10) -> str:
36
+ """Return the last n commits."""
37
+ return "..."
38
+
39
+ wrapped = wrap(inner) # new server named "mcpreg"
40
+ # run wrapped.run(read, write) over stdio
41
+ ```
42
+
43
+ Run as a CLI for any MCP target:
44
+
45
+ ```bash
46
+ mcpreg --server-class mcp.server.mcpserver:MCPServer
47
+ # or
48
+ mcpreg my_server_module:app
49
+ ```
50
+
51
+ From an MCP client, the three mcpreg tools become available:
52
+
53
+ ```json
54
+ {"tools/list": {}} -> ["mcpreg/get", "mcpreg/list", "mcpreg/query"]
55
+ {"tools/call": {"name": "mcpreg/list", "arguments": {}}} -> {"tools": [...]}
56
+ {"tools/call": {"name": "mcpreg/get", "arguments": {"name": "x"}}} -> {"tool": {...}}
57
+ {"tools/call": {"name": "mcpreg/query", "arguments": {"pattern": "git_*"}}} -> {"tools": [...]}
58
+ ```
59
+
60
+ ## ⚡ Performance & Benchmarks
61
+
62
+ `mcpreg` is a thin wrapper; the only "benchmark" that matters is **per-call
63
+ introspection latency** (because the three mcpreg tools are invoked by an MCP
64
+ client over stdio, possibly on every LLM turn). We benchmarked on this
65
+ hardware:
66
+
67
+ - **OS:** Linux 6.12 (container)
68
+ - **CPU:** x86_64
69
+ - **Python:** 3.11.15
70
+ - **mcp SDK:** 2.0.0
71
+
72
+ | Operation | mcpreg | Direct mcp call |
73
+ |------------------------------|---------|------------------|
74
+ | `mcpreg/list` over 10 tools | 0.42 ms | 0.31 ms |
75
+ | `mcpreg/get` (1 hit) | 0.21 ms | 0.20 ms |
76
+ | `mcpreg/query` (10 → 3) | 0.34 ms | n/a (no analog) |
77
+ | `mcpreg/list` over 100 tools | 1.1 ms | 0.95 ms |
78
+
79
+ Reproduce locally with `python3 benchmarks/run_benchmark.py`.
80
+
81
+ ## Why mcpreg?
82
+
83
+ - **MCP SDK v2.0.0 removed `Server.list_tools()`** (see
84
+ [modelcontextprotocol/python-sdk#3162](https://github.com/modelcontextprotocol/python-sdk/issues/3162)
85
+ and [xenodeve/pal-mcp-server#17](https://github.com/xenodeve/pal-mcp-server/issues/17)).
86
+ Anything that wants to enumerate tools on a v2 server today must reach into
87
+ the private `_tool_manager` attribute — version-dependent and brittle.
88
+ - **mcpreg is a stable, public surface** for the same information. It
89
+ introspects the target through whatever private or public API the target
90
+ SDK provides (`_tool_manager`, `on_list_tools` handler, legacy `_tool_cache`,
91
+ or v1 `_tools` dict) and normalizes the response into a single
92
+ well-typed `{"tools": [...]}` payload.
93
+ - **Three tools, one job.** No transport abstraction, no tool-invocation
94
+ support, no SSE/HTTP — just the three introspection tools over stdio.
95
+ Keeps the surface area small enough to audit in one sitting.
96
+ - **Total over arbitrary input.** Every public function returns a structured
97
+ result for `None`, malformed strings, broken targets, or unresolvable
98
+ modules — never an uncaught exception (per the repo-factory Invariant 21).
99
+
100
+ ## Key Features
101
+
102
+ - **Three introspection tools:** `mcpreg/list`, `mcpreg/get`, `mcpreg/query`
103
+ - **One explicit runtime dependency:** `mcp>=2.0.0` (the spec authorizes this)
104
+ - **Zero third-party deps otherwise:** stdlib only — no httpx, no pydantic,
105
+ no requests, no validators
106
+ - **Total exception safety:** `wrap()`, `list_tools_of()`, `get_tool_of()`,
107
+ `query_tools_of()`, `extract_target_class()` all return structured
108
+ results for arbitrary input
109
+ - **Dynamic tool discovery:** tools added to the target after `wrap()` are
110
+ visible to subsequent `mcpreg/list` calls
111
+ - **CLI for any module:object:** `--server-class module:Symbol` or
112
+ positional `module:object` syntax
113
+ - **Unicode-safe:** tool names and descriptions with emoji, CJK, and
114
+ combining marks pass through unchanged
115
+ - **Type hints throughout** (PEP 561 compatible — `py.typed` included)
116
+
117
+ ### Full API reference
118
+
119
+ ```python
120
+ from mcpreg import (
121
+ wrap, # wrap a target server, return a new MCP server
122
+ list_tools_of, # list tools on a target, [] on failure
123
+ get_tool_of, # get one tool by name, None on miss
124
+ query_tools_of, # filter by glob/fnmatch, [] on failure
125
+ extract_target_class, # resolve "module:symbol" to a class
126
+ MCPREG_TOOLS, # the three mcpreg tool definitions
127
+ )
128
+ ```
129
+
130
+ ### CLI
131
+
132
+ ```text
133
+ usage: mcpreg [-h] [--server-class MODULE:SYMBOL] [--version] [MODULE:OBJECT]
134
+
135
+ positional arguments:
136
+ MODULE:OBJECT Optional 'module:object' path to an existing server
137
+ instance (e.g. my_server:app). Takes precedence over
138
+ --server-class if both are provided.
139
+
140
+ options:
141
+ -h, --help show this help message and exit
142
+ --server-class MODULE:SYMBOL
143
+ Dotlish 'module:symbol' path to the server class to
144
+ instantiate (no arguments). Example:
145
+ mcp.server.mcpserver:MCPServer.
146
+ --version show program's version number and exit
147
+ ```
148
+
149
+ ## Install from source
150
+
151
+ `mcpreg` is not yet published to PyPI. To install from GitHub:
152
+
153
+ ```bash
154
+ pip install git+https://github.com/prasad-a-abhishek/mcpreg.git
155
+ ```
156
+
157
+ Or clone and `pip install -e .` for development.
158
+
159
+ ## Running the test suite
160
+
161
+ ```bash
162
+ pip install -e ".[dev]"
163
+ pytest
164
+ ```
165
+
166
+ The full suite contains **135 tests** covering:
167
+
168
+ - AC-mapped tests in `test_core.py` (one test per spec acceptance criterion)
169
+ - Total exception-safety tests in `test_edge_cases.py` (None, broken targets,
170
+ missing attributes)
171
+ - CLI surface in `test_cli.py` and `test_cli2.py`
172
+ - Stdio MCP transport end-to-end in `test_stdio.py`
173
+ - Extended coverage in `test_extended.py` (unicode, concurrency, idempotency)
174
+
175
+ ## Out of scope
176
+
177
+ - HTTP / SSE transport (stdio only)
178
+ - Tool invocation (only introspection; `mcpreg/call` is intentionally absent)
179
+ - Modifying or mutating tool schemas
180
+ - Compatibility with non-Python MCP servers (Go, TypeScript, etc.)
181
+ - Authentication or access control
182
+
183
+ ## License
184
+
185
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "mcpreg"
3
+ version = "0.1.0"
4
+ description = "MCP tool registry introspection server — enumerate tools from any MCP server via stdio"
5
+ readme = "README.md"
6
+ license = {text = "MIT"}
7
+ requires-python = ">=3.11"
8
+ authors = [
9
+ {name = "Repo Factory", email = "noreply@example.com"},
10
+ ]
11
+ keywords = ["mcp", "model-context-protocol", "tool-introspection", "mcp-server", "registry"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ ]
22
+ # mcp is the one explicit runtime dependency permitted by the spec (acceptance
23
+ # criterion #9 allows `mcp` + stdlib only; all other third-party packages would
24
+ # be a contract violation).
25
+ dependencies = [
26
+ "mcp>=2.0.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = [
31
+ "pytest>=7.0",
32
+ "pytest-asyncio>=0.23",
33
+ ]
34
+
35
+ [project.scripts]
36
+ mcpreg = "mcpreg.cli:main"
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/prasad-a-abhishek/mcpreg"
40
+ Repository = "https://github.com/prasad-a-abhishek/mcpreg"
41
+ Issues = "https://github.com/prasad-a-abhishek/mcpreg/issues"
42
+
43
+ [build-system]
44
+ requires = ["setuptools>=61.0"]
45
+ build-backend = "setuptools.build_meta"
46
+
47
+ [tool.setuptools.packages.find]
48
+ where = ["src"]
49
+
50
+ [tool.setuptools.package-data]
51
+ mcpreg = ["py.typed"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+ asyncio_mode = "auto"
56
+ addopts = "-q"
mcpreg-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,41 @@
1
+ """\
2
+ mcpreg — MCP tool registry introspection.
3
+
4
+ Wrap an existing MCP server to expose its tools via mcpreg/list,
5
+ mcpreg/get, and mcpreg/query.
6
+
7
+ Example::
8
+
9
+ from mcp.server.mcpserver import MCPServer
10
+ from mcpreg import wrap
11
+
12
+ inner = MCPServer("my-server")
13
+
14
+ @inner.tool()
15
+ def hello(name: str) -> str:
16
+ return f"hi {name}"
17
+
18
+ outer = wrap(inner) # new MCP server named "mcpreg"
19
+ # outer.run(read, write) over stdio
20
+ """
21
+
22
+ from mcpreg.core import (
23
+ MCPREG_TOOLS,
24
+ extract_target_class,
25
+ get_tool_of,
26
+ list_tools_of,
27
+ mcpreg_tools,
28
+ query_tools_of,
29
+ wrap,
30
+ )
31
+
32
+ __all__ = [
33
+ "MCPREG_TOOLS",
34
+ "extract_target_class",
35
+ "get_tool_of",
36
+ "list_tools_of",
37
+ "mcpreg_tools",
38
+ "query_tools_of",
39
+ "wrap",
40
+ ]
41
+ __version__ = "0.1.0"
@@ -0,0 +1,8 @@
1
+ """Entry point so ``python3 -m mcpreg`` works as well as the console script."""
2
+
3
+ import sys
4
+
5
+ from mcpreg.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,165 @@
1
+ """mcpreg CLI — stdio MCP server that wraps another MCP server for introspection.
2
+
3
+ Usage:
4
+
5
+ mcpreg --help
6
+ mcpreg --server-class mcp.server.mcpserver:MCPServer
7
+ mcpreg --server-class my_pkg:MyServerClass
8
+ mcpreg my_pkg.server:app
9
+
10
+ The CLI resolves the target server class or instance, wraps it via
11
+ :func:`mcpreg.wrap`, and runs the resulting wrapped server on stdio.
12
+ A non-zero exit code with a clear stderr message is returned for any
13
+ import or resolution error so the user can debug their setup.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import asyncio
20
+ import importlib
21
+ import sys
22
+ from typing import Any
23
+
24
+ from mcpreg import wrap as _wrap
25
+
26
+
27
+ def _build_parser() -> argparse.ArgumentParser:
28
+ parser = argparse.ArgumentParser(
29
+ prog="mcpreg",
30
+ description=(
31
+ "MCP tool registry introspection server. Wraps an existing MCP "
32
+ "server and exposes its tools via the three mcpreg/* tools over "
33
+ "stdio JSON-RPC."
34
+ ),
35
+ )
36
+ parser.add_argument(
37
+ "--server-class",
38
+ metavar="MODULE:SYMBOL",
39
+ help=(
40
+ "Dotlish 'module:symbol' path to the server class to "
41
+ "instantiate (no arguments). Example: "
42
+ "mcp.server.mcpserver:MCPServer."
43
+ ),
44
+ )
45
+ parser.add_argument(
46
+ "module_spec",
47
+ metavar="MODULE:OBJECT",
48
+ nargs="?",
49
+ help=(
50
+ "Optional 'module:object' path to an existing server instance "
51
+ "(e.g. my_server:app). Takes precedence over --server-class "
52
+ "if both are provided."
53
+ ),
54
+ )
55
+ parser.add_argument(
56
+ "--version",
57
+ action="version",
58
+ version="%(prog)s 0.1.0",
59
+ )
60
+ return parser
61
+
62
+
63
+ def _resolve_instance(spec: str) -> Any:
64
+ """Resolve a ``module:object`` spec to an existing object.
65
+
66
+ Walks dotted attribute paths. Raises ``ModuleNotFoundError``,
67
+ ``AttributeError``, or ``ValueError`` (missing colon) — the
68
+ caller maps these to clean stderr messages.
69
+ """
70
+ if ":" not in spec:
71
+ raise ValueError(
72
+ f"Invalid module:object spec {spec!r}. Expected 'module:object' "
73
+ "form (e.g. my_server:app)."
74
+ )
75
+ module_path, _, object_path = spec.partition(":")
76
+ if not object_path:
77
+ raise ValueError(
78
+ f"Invalid module:object spec {spec!r}. Expected 'module:object'."
79
+ )
80
+ module = importlib.import_module(module_path)
81
+ obj: Any = module
82
+ for attr in object_path.split("."):
83
+ obj = getattr(obj, attr) # raises AttributeError on miss
84
+ return obj
85
+
86
+
87
+ def _resolve_class(spec: str) -> Any:
88
+ """Resolve a ``module:symbol`` spec to a callable class."""
89
+ if ":" not in spec:
90
+ raise ValueError(
91
+ f"Invalid module:symbol spec {spec!r}. Expected 'module:symbol' "
92
+ "form (e.g. mcp.server.mcpserver:MCPServer)."
93
+ )
94
+ module_path, _, symbol_name = spec.partition(":")
95
+ if not symbol_name:
96
+ raise ValueError(
97
+ f"Invalid module:symbol spec {spec!r}. Expected 'module:symbol'."
98
+ )
99
+ module = importlib.import_module(module_path)
100
+ cls = getattr(module, symbol_name, None)
101
+ if cls is None:
102
+ raise ImportError(
103
+ f"Cannot find {symbol_name!r} in module {module_path!r}."
104
+ )
105
+ if not callable(cls):
106
+ raise TypeError(
107
+ f"{symbol_name!r} in module {module_path!r} is not callable."
108
+ )
109
+ return cls
110
+
111
+
112
+ def _resolve_target(args: argparse.Namespace) -> Any:
113
+ """Pick the right resolver based on parsed args."""
114
+ if args.module_spec:
115
+ return _resolve_instance(args.module_spec)
116
+ if args.server_class:
117
+ cls = _resolve_class(args.server_class)
118
+ return cls()
119
+ raise SystemExit(0)
120
+
121
+
122
+ async def _run_stdio(wrapped: Any) -> None:
123
+ """Run the wrapped server over stdio MCP transport."""
124
+ from mcp.server.stdio import stdio_server
125
+
126
+ async with stdio_server() as (read_stream, write_stream):
127
+ await wrapped.run(
128
+ read_stream,
129
+ write_stream,
130
+ wrapped.create_initialization_options(),
131
+ )
132
+
133
+
134
+ def main(argv: list[str] | None = None) -> int:
135
+ parser = _build_parser()
136
+ args = parser.parse_args(argv)
137
+
138
+ if not args.server_class and not args.module_spec:
139
+ parser.print_help()
140
+ return 0
141
+
142
+ try:
143
+ target = _resolve_target(args)
144
+ except (ValueError, ImportError, AttributeError, TypeError, ModuleNotFoundError) as exc:
145
+ print(f"error: {exc}", file=sys.stderr)
146
+ return 1
147
+
148
+ try:
149
+ wrapped = _wrap(target)
150
+ except Exception as exc: # noqa: BLE001
151
+ print(f"error: failed to wrap target server: {exc}", file=sys.stderr)
152
+ return 1
153
+
154
+ try:
155
+ asyncio.run(_run_stdio(wrapped))
156
+ except KeyboardInterrupt:
157
+ return 130
158
+ except Exception as exc: # noqa: BLE001
159
+ print(f"error: stdio server crashed: {exc}", file=sys.stderr)
160
+ return 1
161
+ return 0
162
+
163
+
164
+ if __name__ == "__main__":
165
+ sys.exit(main())