mcpreg 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.
- mcpreg/__init__.py +41 -0
- mcpreg/__main__.py +8 -0
- mcpreg/cli.py +165 -0
- mcpreg/core.py +483 -0
- mcpreg/py.typed +1 -0
- mcpreg/wrapper.py +26 -0
- mcpreg-0.1.0.dist-info/METADATA +212 -0
- mcpreg-0.1.0.dist-info/RECORD +12 -0
- mcpreg-0.1.0.dist-info/WHEEL +5 -0
- mcpreg-0.1.0.dist-info/entry_points.txt +2 -0
- mcpreg-0.1.0.dist-info/licenses/LICENSE +21 -0
- mcpreg-0.1.0.dist-info/top_level.txt +1 -0
mcpreg/__init__.py
ADDED
|
@@ -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"
|
mcpreg/__main__.py
ADDED
mcpreg/cli.py
ADDED
|
@@ -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())
|
mcpreg/core.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""\
|
|
2
|
+
mcpreg — Core library: wrap an existing MCP server and expose introspection.
|
|
3
|
+
|
|
4
|
+
The package public surface lives in ``mcpreg.__init__`` and is implemented
|
|
5
|
+
in this module:
|
|
6
|
+
|
|
7
|
+
* :func:`wrap` — wrap an existing MCP server and return a new server
|
|
8
|
+
that exposes the three ``mcpreg/*`` introspection tools.
|
|
9
|
+
* :func:`list_tools_of` — synchronous, total, ``[]``-on-failure tool
|
|
10
|
+
enumerator for any compatible target.
|
|
11
|
+
* :func:`get_tool_of` — synchronous, total single-tool lookup.
|
|
12
|
+
* :func:`query_tools_of` — synchronous, total glob-filtered tool lookup.
|
|
13
|
+
* :func:`extract_target_class` — resolve a ``module:symbol`` spec to a
|
|
14
|
+
class object, raising the right error type for each failure mode.
|
|
15
|
+
* :data:`MCPREG_TOOLS` — the three mcpreg tool definitions (name /
|
|
16
|
+
description / inputSchema) for downstream consumers that want to
|
|
17
|
+
re-publish them.
|
|
18
|
+
|
|
19
|
+
All public functions in this module are total over arbitrary input
|
|
20
|
+
(Invariant 21): passing ``None``, malformed strings, or unresolvable
|
|
21
|
+
modules never raises an uncaught exception; it returns a structured
|
|
22
|
+
empty/error result.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import fnmatch
|
|
28
|
+
import asyncio
|
|
29
|
+
import importlib
|
|
30
|
+
import json as _json
|
|
31
|
+
from typing import Any, Callable
|
|
32
|
+
|
|
33
|
+
from mcp.server import Server as _Server
|
|
34
|
+
from mcp.types import (
|
|
35
|
+
CallToolRequestParams,
|
|
36
|
+
CallToolResult,
|
|
37
|
+
ListToolsResult,
|
|
38
|
+
PaginatedRequestParams,
|
|
39
|
+
TextContent,
|
|
40
|
+
Tool,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"wrap",
|
|
46
|
+
"list_tools_of",
|
|
47
|
+
"get_tool_of",
|
|
48
|
+
"query_tools_of",
|
|
49
|
+
"extract_target_class",
|
|
50
|
+
"MCPREG_TOOLS",
|
|
51
|
+
"mcpreg_tools",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# JSON helpers
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _json_dumps(obj: Any) -> str:
|
|
61
|
+
"""Serialize obj to a compact JSON string."""
|
|
62
|
+
return _json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _ok_content(payload: Any) -> list[TextContent]:
|
|
66
|
+
return [TextContent(type="text", text=_json_dumps(payload))]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _err_content(code: int, message: str) -> list[TextContent]:
|
|
70
|
+
return [
|
|
71
|
+
TextContent(
|
|
72
|
+
type="text",
|
|
73
|
+
text=_json_dumps({"error": {"code": code, "message": message}}),
|
|
74
|
+
)
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# mcpreg tool definitions
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
MCPREG_TOOLS: list[dict[str, Any]] = [
|
|
84
|
+
{
|
|
85
|
+
"name": "mcpreg/list",
|
|
86
|
+
"description": (
|
|
87
|
+
"List every tool registered on the wrapped MCP server. "
|
|
88
|
+
"Returns a JSON object with a 'tools' array where each entry "
|
|
89
|
+
"has 'name', 'description', and 'inputSchema'."
|
|
90
|
+
),
|
|
91
|
+
"inputSchema": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"properties": {},
|
|
94
|
+
"additionalProperties": False,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "mcpreg/get",
|
|
99
|
+
"description": (
|
|
100
|
+
"Retrieve the full metadata of a single tool by exact name. "
|
|
101
|
+
"Returns the tool object as JSON, or a JSON-RPC error if not found."
|
|
102
|
+
),
|
|
103
|
+
"inputSchema": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"properties": {
|
|
106
|
+
"name": {
|
|
107
|
+
"type": "string",
|
|
108
|
+
"description": "Exact name of the tool to retrieve.",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
"required": ["name"],
|
|
112
|
+
"additionalProperties": False,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "mcpreg/query",
|
|
117
|
+
"description": (
|
|
118
|
+
"Filter the wrapped server's tools by a glob/fnmatch pattern "
|
|
119
|
+
"(e.g. 'git_*'). Returns a JSON object with a 'tools' array."
|
|
120
|
+
),
|
|
121
|
+
"inputSchema": {
|
|
122
|
+
"type": "object",
|
|
123
|
+
"properties": {
|
|
124
|
+
"pattern": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"description": "Glob pattern (fnmatch) to filter tool names.",
|
|
127
|
+
"default": "*",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
"additionalProperties": False,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def mcpreg_tools() -> list[Tool]:
|
|
137
|
+
"""Return the three mcpreg introspection tool definitions as Tool objects."""
|
|
138
|
+
return [
|
|
139
|
+
Tool(
|
|
140
|
+
name=t["name"],
|
|
141
|
+
description=t["description"],
|
|
142
|
+
inputSchema=t["inputSchema"],
|
|
143
|
+
)
|
|
144
|
+
for t in MCPREG_TOOLS
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# Introspection helpers — total over arbitrary input
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _tool_to_dict(tool: Any) -> dict[str, Any]:
|
|
154
|
+
"""Convert any mcp Tool-like object to a plain dict."""
|
|
155
|
+
name = getattr(tool, "name", "") or ""
|
|
156
|
+
description = getattr(tool, "description", "") or ""
|
|
157
|
+
schema: Any = None
|
|
158
|
+
for attr in ("inputSchema", "input_schema", "parameters"):
|
|
159
|
+
candidate = getattr(tool, attr, None)
|
|
160
|
+
if candidate:
|
|
161
|
+
schema = candidate
|
|
162
|
+
break
|
|
163
|
+
if not isinstance(schema, dict):
|
|
164
|
+
schema = {"type": "object", "properties": {}}
|
|
165
|
+
return {
|
|
166
|
+
"name": str(name),
|
|
167
|
+
"description": str(description),
|
|
168
|
+
"inputSchema": schema,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def _list_tools_async(target: Any) -> list[dict[str, Any]]:
|
|
173
|
+
"""Asynchronously enumerate tools on ``target``.
|
|
174
|
+
|
|
175
|
+
Total over arbitrary input. Returns ``[]`` on any failure (None target,
|
|
176
|
+
broken tool manager, missing on_list_tools handler, etc.).
|
|
177
|
+
"""
|
|
178
|
+
if target is None:
|
|
179
|
+
return []
|
|
180
|
+
|
|
181
|
+
# 1) v2 MCPServer path — has _tool_manager with list_tools() (sync or async)
|
|
182
|
+
try:
|
|
183
|
+
tm = getattr(target, "_tool_manager", None)
|
|
184
|
+
except Exception:
|
|
185
|
+
tm = None
|
|
186
|
+
if tm is not None:
|
|
187
|
+
try:
|
|
188
|
+
result = tm.list_tools()
|
|
189
|
+
if hasattr(result, "__await__"):
|
|
190
|
+
result = await result
|
|
191
|
+
return [_tool_to_dict(t) for t in (result or [])]
|
|
192
|
+
except Exception:
|
|
193
|
+
return []
|
|
194
|
+
|
|
195
|
+
# 2) Plain Server path — check for on_list_tools handler via _request_handlers
|
|
196
|
+
handlers = getattr(target, "_request_handlers", None)
|
|
197
|
+
if isinstance(handlers, dict):
|
|
198
|
+
entry = handlers.get("tools/list")
|
|
199
|
+
if entry is not None:
|
|
200
|
+
handler = getattr(entry, "handler", None)
|
|
201
|
+
if handler is not None:
|
|
202
|
+
try:
|
|
203
|
+
result = handler(None, PaginatedRequestParams())
|
|
204
|
+
if hasattr(result, "__await__"):
|
|
205
|
+
result = await result
|
|
206
|
+
tools = getattr(result, "tools", None) or []
|
|
207
|
+
return [_tool_to_dict(t) for t in tools]
|
|
208
|
+
except Exception:
|
|
209
|
+
return []
|
|
210
|
+
|
|
211
|
+
# 3) Legacy _tool_cache (older SDK versions)
|
|
212
|
+
cache = getattr(target, "_tool_cache", None)
|
|
213
|
+
if isinstance(cache, dict):
|
|
214
|
+
return [_tool_to_dict(t) for t in cache.values()]
|
|
215
|
+
|
|
216
|
+
# 4) Legacy _tools dict
|
|
217
|
+
legacy = getattr(target, "_tools", None)
|
|
218
|
+
if isinstance(legacy, dict):
|
|
219
|
+
return [_tool_to_dict(t) for t in legacy.values()]
|
|
220
|
+
|
|
221
|
+
return []
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _list_tools_sync(target: Any) -> list[dict[str, Any]]:
|
|
225
|
+
"""Synchronous enumerator for callers outside the event loop.
|
|
226
|
+
|
|
227
|
+
When called from inside a running loop, this function falls back to
|
|
228
|
+
a best-effort sync walk of the target's tool manager and handlers
|
|
229
|
+
(it closes any coroutine it encounters rather than awaiting it —
|
|
230
|
+
awaiting would require the running loop). When called from a
|
|
231
|
+
non-loop context, it uses ``asyncio.run`` to drive the async path.
|
|
232
|
+
"""
|
|
233
|
+
if target is None:
|
|
234
|
+
return []
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
loop = asyncio.get_running_loop()
|
|
238
|
+
except RuntimeError:
|
|
239
|
+
# No running loop — drive the async helper via asyncio.run
|
|
240
|
+
try:
|
|
241
|
+
return asyncio.run(_list_tools_async(target))
|
|
242
|
+
except Exception:
|
|
243
|
+
return []
|
|
244
|
+
# Inside a running loop — return what we can synchronously
|
|
245
|
+
try:
|
|
246
|
+
tm = getattr(target, "_tool_manager", None)
|
|
247
|
+
except Exception:
|
|
248
|
+
tm = None
|
|
249
|
+
if tm is not None:
|
|
250
|
+
try:
|
|
251
|
+
result = tm.list_tools()
|
|
252
|
+
if hasattr(result, "__await__"):
|
|
253
|
+
try:
|
|
254
|
+
result.close()
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
return []
|
|
258
|
+
return [_tool_to_dict(t) for t in (result or [])]
|
|
259
|
+
except Exception:
|
|
260
|
+
return []
|
|
261
|
+
handlers = getattr(target, "_request_handlers", None)
|
|
262
|
+
if isinstance(handlers, dict):
|
|
263
|
+
entry = handlers.get("tools/list")
|
|
264
|
+
if entry is not None:
|
|
265
|
+
handler = getattr(entry, "handler", None)
|
|
266
|
+
if handler is not None:
|
|
267
|
+
try:
|
|
268
|
+
result = handler(None, PaginatedRequestParams())
|
|
269
|
+
if hasattr(result, "__await__"):
|
|
270
|
+
try:
|
|
271
|
+
result.close()
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
return []
|
|
275
|
+
tools = getattr(result, "tools", None) or []
|
|
276
|
+
return [_tool_to_dict(t) for t in tools]
|
|
277
|
+
except Exception:
|
|
278
|
+
return []
|
|
279
|
+
cache = getattr(target, "_tool_cache", None)
|
|
280
|
+
if isinstance(cache, dict):
|
|
281
|
+
return [_tool_to_dict(t) for t in cache.values()]
|
|
282
|
+
legacy = getattr(target, "_tools", None)
|
|
283
|
+
if isinstance(legacy, dict):
|
|
284
|
+
return [_tool_to_dict(t) for t in legacy.values()]
|
|
285
|
+
return []
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def list_tools_of(target: Any) -> list[dict[str, Any]]:
|
|
289
|
+
"""Synchronous, total tool enumerator. Returns ``[]`` for any failure."""
|
|
290
|
+
try:
|
|
291
|
+
return _list_tools_sync(target)
|
|
292
|
+
except Exception:
|
|
293
|
+
return []
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_tool_of(target: Any, name: Any) -> dict[str, Any] | None:
|
|
297
|
+
"""Return the single tool dict for ``name`` or ``None`` if not found / invalid."""
|
|
298
|
+
if not isinstance(name, str) or not name:
|
|
299
|
+
return None
|
|
300
|
+
try:
|
|
301
|
+
for t in list_tools_of(target):
|
|
302
|
+
if isinstance(t, dict) and t.get("name") == name:
|
|
303
|
+
return t
|
|
304
|
+
except Exception:
|
|
305
|
+
return None
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def query_tools_of(target: Any, pattern: Any) -> list[dict[str, Any]]:
|
|
310
|
+
"""Filter target tools by a glob/fnmatch pattern. Total over arbitrary input.
|
|
311
|
+
|
|
312
|
+
Empty / non-string / malformed patterns default to ``"*"`` (match all).
|
|
313
|
+
"""
|
|
314
|
+
pat = pattern if isinstance(pattern, str) and pattern else "*"
|
|
315
|
+
try:
|
|
316
|
+
tools = list_tools_of(target)
|
|
317
|
+
except Exception:
|
|
318
|
+
return []
|
|
319
|
+
try:
|
|
320
|
+
return [t for t in tools if fnmatch.fnmatch(t.get("name", ""), pat)]
|
|
321
|
+
except Exception:
|
|
322
|
+
return []
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ---------------------------------------------------------------------------
|
|
326
|
+
# module:symbol resolver (used by the CLI)
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def extract_target_class(spec: Any) -> Any:
|
|
331
|
+
"""Load a class from a ``module:symbol`` spec string.
|
|
332
|
+
|
|
333
|
+
Examples
|
|
334
|
+
--------
|
|
335
|
+
>>> extract_target_class("mcp.server.mcpserver:MCPServer")
|
|
336
|
+
<class 'mcp.server.mcpserver.MCPServer'>
|
|
337
|
+
|
|
338
|
+
Raises
|
|
339
|
+
------
|
|
340
|
+
ValueError
|
|
341
|
+
if ``spec`` is not a string in ``module:symbol`` form.
|
|
342
|
+
ImportError
|
|
343
|
+
if the module is not importable, or the symbol is not present.
|
|
344
|
+
"""
|
|
345
|
+
if not isinstance(spec, str):
|
|
346
|
+
raise ValueError(
|
|
347
|
+
f"Invalid spec {spec!r}. Expected 'module:symbol' form (a string)."
|
|
348
|
+
)
|
|
349
|
+
if ":" not in spec:
|
|
350
|
+
raise ValueError(
|
|
351
|
+
f"Invalid spec {spec!r}. Expected 'module:symbol' form (missing colon)."
|
|
352
|
+
)
|
|
353
|
+
module_path, _, symbol_name = spec.partition(":")
|
|
354
|
+
if not module_path:
|
|
355
|
+
raise ValueError(
|
|
356
|
+
f"Invalid spec {spec!r}. Expected 'module:symbol' form (empty module)."
|
|
357
|
+
)
|
|
358
|
+
if not symbol_name:
|
|
359
|
+
raise ValueError(
|
|
360
|
+
f"Invalid spec {spec!r}. Expected 'module:symbol' form (empty symbol)."
|
|
361
|
+
)
|
|
362
|
+
try:
|
|
363
|
+
module = importlib.import_module(module_path)
|
|
364
|
+
except ImportError as exc:
|
|
365
|
+
raise ImportError(
|
|
366
|
+
f"Module {module_path!r} is not installed or cannot be imported: {exc}"
|
|
367
|
+
)
|
|
368
|
+
obj = getattr(module, symbol_name, None)
|
|
369
|
+
if obj is None:
|
|
370
|
+
raise ImportError(
|
|
371
|
+
f"Cannot find {symbol_name!r} in module {module_path!r}."
|
|
372
|
+
)
|
|
373
|
+
return obj
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
# Public wrap() — produce a new MCP server exposing mcpreg/list, /get, /query
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def wrap(target: Any) -> _Server:
|
|
382
|
+
"""\
|
|
383
|
+
Wrap an existing MCP server and return a new MCP server that exposes
|
|
384
|
+
three introspection tools: ``mcpreg/list``, ``mcpreg/get``, and
|
|
385
|
+
``mcpreg/query``.
|
|
386
|
+
|
|
387
|
+
The original server is never mutated.
|
|
388
|
+
|
|
389
|
+
Parameters
|
|
390
|
+
----------
|
|
391
|
+
target
|
|
392
|
+
An MCP server instance. Supported shapes:
|
|
393
|
+
|
|
394
|
+
* ``mcp.server.mcpserver.MCPServer`` (v2) — tools discovered via
|
|
395
|
+
``_tool_manager.list_tools()``.
|
|
396
|
+
* ``mcp.server.Server`` (v2) — tools discovered via the
|
|
397
|
+
``on_list_tools`` handler registered in ``_request_handlers``.
|
|
398
|
+
* Objects with ``_tool_cache`` or ``_tools`` dicts (legacy SDKs).
|
|
399
|
+
|
|
400
|
+
Returns
|
|
401
|
+
-------
|
|
402
|
+
mcp.server.Server
|
|
403
|
+
A new server named ``mcpreg`` that advertises only the three
|
|
404
|
+
``mcpreg/*`` tools and dispatches them to the wrapped target.
|
|
405
|
+
|
|
406
|
+
Notes
|
|
407
|
+
-----
|
|
408
|
+
The wrapper NEVER mutates the target. If the target is not
|
|
409
|
+
introspectable, the wrapper returns a server whose ``mcpreg/list``
|
|
410
|
+
response is ``{"tools": []}`` — it does not raise.
|
|
411
|
+
"""
|
|
412
|
+
if target is None:
|
|
413
|
+
raise ValueError("mcpreg.wrap() requires a target server, got None")
|
|
414
|
+
|
|
415
|
+
async def on_list_tools(
|
|
416
|
+
ctx: Any, params: PaginatedRequestParams | None
|
|
417
|
+
) -> ListToolsResult:
|
|
418
|
+
return ListToolsResult(tools=mcpreg_tools())
|
|
419
|
+
|
|
420
|
+
async def on_call_tool(
|
|
421
|
+
ctx: Any, params: CallToolRequestParams
|
|
422
|
+
) -> CallToolResult:
|
|
423
|
+
name = getattr(params, "name", "") or ""
|
|
424
|
+
arguments = getattr(params, "arguments", None) or {}
|
|
425
|
+
|
|
426
|
+
if name == "mcpreg/list":
|
|
427
|
+
tools = await _list_tools_async(target)
|
|
428
|
+
return CallToolResult(content=_ok_content({"tools": tools}))
|
|
429
|
+
|
|
430
|
+
if name == "mcpreg/get":
|
|
431
|
+
arg_name = (
|
|
432
|
+
arguments.get("name", "") if isinstance(arguments, dict) else ""
|
|
433
|
+
)
|
|
434
|
+
if not isinstance(arg_name, str) or not arg_name:
|
|
435
|
+
return CallToolResult(
|
|
436
|
+
content=_err_content(
|
|
437
|
+
-32602,
|
|
438
|
+
"mcpreg/get requires a string 'name' argument",
|
|
439
|
+
),
|
|
440
|
+
is_error=True,
|
|
441
|
+
)
|
|
442
|
+
tools = await _list_tools_async(target)
|
|
443
|
+
for t in tools:
|
|
444
|
+
if t.get("name") == arg_name:
|
|
445
|
+
return CallToolResult(content=_ok_content({"tool": t}))
|
|
446
|
+
return CallToolResult(
|
|
447
|
+
content=_err_content(-32602, f"Tool not found: {arg_name}"),
|
|
448
|
+
is_error=True,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
if name == "mcpreg/query":
|
|
452
|
+
pattern = (
|
|
453
|
+
arguments.get("pattern", "*") if isinstance(arguments, dict) else "*"
|
|
454
|
+
)
|
|
455
|
+
if not isinstance(pattern, str) or not pattern:
|
|
456
|
+
pattern = "*"
|
|
457
|
+
tools = await _list_tools_async(target)
|
|
458
|
+
matched = [
|
|
459
|
+
t for t in tools
|
|
460
|
+
if fnmatch.fnmatch(t.get("name", ""), pattern)
|
|
461
|
+
]
|
|
462
|
+
return CallToolResult(content=_ok_content({"tools": matched}))
|
|
463
|
+
|
|
464
|
+
return CallToolResult(
|
|
465
|
+
content=_err_content(
|
|
466
|
+
-32601,
|
|
467
|
+
f"Unknown tool: {name!r}. "
|
|
468
|
+
"mcpreg exposes only mcpreg/list, mcpreg/get, and mcpreg/query.",
|
|
469
|
+
),
|
|
470
|
+
is_error=True,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
return _Server(
|
|
474
|
+
"mcpreg",
|
|
475
|
+
version="0.1.0",
|
|
476
|
+
instructions=(
|
|
477
|
+
"MCP tool registry introspection server. Wraps another MCP "
|
|
478
|
+
"server and exposes its tools via mcpreg/list, mcpreg/get, "
|
|
479
|
+
"and mcpreg/query."
|
|
480
|
+
),
|
|
481
|
+
on_list_tools=on_list_tools,
|
|
482
|
+
on_call_tool=on_call_tool,
|
|
483
|
+
)
|
mcpreg/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Marker file for PEP 561 — mcpreg ships inline type annotations."""
|
mcpreg/wrapper.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Backward-compat shim — the real implementation lives in ``mcpreg.core``.
|
|
2
|
+
|
|
3
|
+
This module is kept so that ``from mcpreg.wrapper import wrap`` and
|
|
4
|
+
similar historical imports continue to work. New code should import
|
|
5
|
+
from ``mcpreg`` directly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from mcpreg.core import ( # noqa: F401
|
|
9
|
+
MCPREG_TOOLS,
|
|
10
|
+
extract_target_class,
|
|
11
|
+
get_tool_of,
|
|
12
|
+
list_tools_of,
|
|
13
|
+
mcpreg_tools,
|
|
14
|
+
query_tools_of,
|
|
15
|
+
wrap,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"MCPREG_TOOLS",
|
|
20
|
+
"extract_target_class",
|
|
21
|
+
"get_tool_of",
|
|
22
|
+
"list_tools_of",
|
|
23
|
+
"mcpreg_tools",
|
|
24
|
+
"query_tools_of",
|
|
25
|
+
"wrap",
|
|
26
|
+
]
|
|
@@ -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
|
+
[](https://pypi.org)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
[](https://www.python.org/)
|
|
33
|
+
[](tests/)
|
|
34
|
+
[](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).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
mcpreg/__init__.py,sha256=ftJ6xNDYY4WVLyXorUnv1e2CbyIQJzk7sgAZfCY-nwg,787
|
|
2
|
+
mcpreg/__main__.py,sha256=XQN4g8SAH-KsPgOuDE8aiAAQSMxmVTb-6hkoKCNa5i0,170
|
|
3
|
+
mcpreg/cli.py,sha256=la6HqbG6Zz2AuSM4kDRZydwiZHXod0kaW6YHAwIn1b4,5017
|
|
4
|
+
mcpreg/core.py,sha256=iF2umG_ccfdnppBIlf0JLxA0SOtLVhpgq6h2qgoaNbw,16186
|
|
5
|
+
mcpreg/py.typed,sha256=pNYVC3z0AtnRR9Su3p_4UQ0oKTVijl80-KYYtdx-BR4,72
|
|
6
|
+
mcpreg/wrapper.py,sha256=bf3bxo0dX9akY-AQnC7ED9CQY6lNeTzjMIQ1r2Wb57Y,573
|
|
7
|
+
mcpreg-0.1.0.dist-info/licenses/LICENSE,sha256=FEO3Vgpd6moNnacnqGNTvfFZQxSqp78685C0c8NgxeU,1069
|
|
8
|
+
mcpreg-0.1.0.dist-info/METADATA,sha256=Oy13t5eUafV_yaytCnl0SKHAddy-1IJcoEwVE7AJ-bI,8128
|
|
9
|
+
mcpreg-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
10
|
+
mcpreg-0.1.0.dist-info/entry_points.txt,sha256=QajPr9vCo9xOJstTToNNSerPzlS9YUsInSleOFm8PHE,43
|
|
11
|
+
mcpreg-0.1.0.dist-info/top_level.txt,sha256=Tn4EfbBVfdKwoKvBPqpmjKj_Rlh6MOVqkuR2M5ZOH4k,7
|
|
12
|
+
mcpreg-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mcpreg
|