Skip to content

Commit d7e4fc0

Browse files
authored
Add support for callables (#15)
1 parent 246af0a commit d7e4fc0

File tree

4 files changed

+68
-1
lines changed

4 files changed

+68
-1
lines changed

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1313
- Add support for Typer
1414
- Add support for Argparse
1515
- Always set the `PYCLI_MCP_TOOL_NAME` environment variable when running commands
16+
- Add support for referring to a callable object that returns a command
1617

1718
## 0.2.0 - 2025-06-28
1819

src/pycli_mcp/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def pycli_mcp(
116116
|_| \\__ |\\______)_______)_| |_| |_|\\______)_|
117117
(____/
118118
119-
Run an MCP server using a list of import paths to commands:
119+
Run an MCP server using a list of import paths to commands or callable objects that return a command:
120120
121121
\b
122122
```

src/pycli_mcp/metadata/query.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,26 @@ def walk_commands(
7777
if aggregate is None:
7878
aggregate = "root"
7979

80+
yield from _walk_commands(
81+
command,
82+
aggregate=aggregate,
83+
name=name,
84+
include=include,
85+
exclude=exclude,
86+
strict_types=strict_types,
87+
)
88+
89+
90+
def _walk_commands(
91+
command: Any,
92+
*,
93+
aggregate: Literal["root", "group", "none"],
94+
name: str | None,
95+
include: str | re.Pattern | None,
96+
exclude: str | re.Pattern | None,
97+
strict_types: bool,
98+
depth: int = 0,
99+
) -> Iterator[CommandMetadata]:
80100
# Click
81101
if hasattr(command, "context_class"):
82102
from pycli_mcp.metadata.types.click import walk_commands as walk_click_commands
@@ -124,5 +144,21 @@ def walk_commands(
124144
)
125145
return
126146

147+
if callable(command):
148+
if depth > 0:
149+
msg = "Callable did not return a known command type"
150+
raise NotImplementedError(msg)
151+
152+
yield from _walk_commands(
153+
command(),
154+
aggregate=aggregate,
155+
name=name,
156+
include=include,
157+
exclude=exclude,
158+
strict_types=strict_types,
159+
depth=depth + 1,
160+
)
161+
return
162+
127163
msg = f"Unsupported command type: {type(command)}"
128164
raise NotImplementedError(msg)

tests/click/test_collection.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,36 @@ def cli() -> None:
4141
assert not metadata.options
4242

4343

44+
def test_dynamic() -> None:
45+
from pycli_mcp.metadata.query import walk_commands
46+
47+
def func() -> click.Command:
48+
@click.command()
49+
def cli() -> None:
50+
# fmt: off
51+
"""
52+
53+
text
54+
nested
55+
56+
"""
57+
# fmt: on
58+
59+
return cli
60+
61+
commands = list(walk_commands(func, aggregate="none"))
62+
assert len(commands) == 1, commands
63+
64+
metadata = commands[0]
65+
assert metadata.path == "cli"
66+
assert metadata.schema == {
67+
"description": "text\n nested",
68+
"properties": {},
69+
"title": "cli",
70+
"type": "object",
71+
}
72+
73+
4474
def test_nested_commands() -> None:
4575
@click.group()
4676
def cli() -> None:

0 commit comments

Comments
 (0)