Skip to content

Commit 2b68ab1

Browse files
authored
Add EnrichParameter to document tool params (#113)
1 parent d147f71 commit 2b68ab1

File tree

8 files changed

+152
-0
lines changed

8 files changed

+152
-0
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,20 @@ async def get_customer(cid: int, ctx: EnrichContext) -> Customer:
382382
return await ctx.cache.get_or_set(f"customer:{cid}", fetch)
383383
```
384384

385+
### 🧭 Parameter Hints
386+
387+
Provide examples and metadata for tool parameters using `EnrichParameter`:
388+
389+
```python
390+
from enrichmcp import EnrichParameter
391+
392+
@app.retrieve
393+
async def greet_user(name: str = EnrichParameter(description="user name", examples=["bob"])) -> str:
394+
return f"Hello {name}"
395+
```
396+
397+
Tool descriptions will include the parameter type, description, and examples.
398+
385399
### 🌐 HTTP & SSE Support
386400

387401
Serve your API over standard output (default), SSE, or HTTP:

docs/api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ class User(EnrichModel):
4141
### [EnrichContext](api/context.md)
4242
Context object with request scoped utilities including caching.
4343

44+
### [EnrichParameter](api/parameter.md)
45+
Attach metadata like descriptions and examples to function parameters.
46+
4447
### [Cache](api/cache.md)
4548
Request, user, and global scoped caching utilities.
4649

docs/api/parameter.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Parameter Metadata
2+
3+
`EnrichParameter` attaches hints like descriptions and examples to function parameters.
4+
When a parameter's default value is an instance of `EnrichParameter`, those hints
5+
are appended to the generated tool description (except for parameters typed as
6+
`EnrichContext`).
7+
8+
```python
9+
from enrichmcp import EnrichParameter
10+
11+
@app.retrieve
12+
async def greet(name: str = EnrichParameter(description="user name", examples=["bob"])) -> str:
13+
return f"Hello {name}"
14+
```

docs/getting-started.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,20 @@ async def get_user(user_id: int, ctx: EnrichContext) -> User:
191191

192192
Context is automatically injected when you add a parameter typed as `EnrichContext`.
193193

194+
## Parameter Hints
195+
196+
You can attach descriptions and examples to resource parameters using `EnrichParameter`:
197+
198+
```python
199+
from enrichmcp import EnrichParameter
200+
201+
@app.retrieve
202+
async def greet(name: str = EnrichParameter(description="user name", examples=["bob"])) -> str:
203+
return f"Hello {name}"
204+
```
205+
206+
These hints are appended to the generated tool description so agents know how to call the resource.
207+
194208
## Using Existing SQLAlchemy Models
195209

196210
Have a project full of SQLAlchemy models already? You can expose them as an MCP

src/enrichmcp/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from .entity import EnrichModel
3131
from .lifespan import combine_lifespans
3232
from .pagination import CursorParams, CursorResult, PageResult, PaginatedResult, PaginationParams
33+
from .parameter import EnrichParameter
3334
from .relationship import (
3435
Relationship,
3536
)
@@ -52,6 +53,7 @@
5253
"EnrichContext",
5354
"EnrichMCP",
5455
"EnrichModel",
56+
"EnrichParameter",
5557
"MemoryCache",
5658
"PageResult",
5759
"PaginatedResult",

src/enrichmcp/app.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Provides the EnrichMCP class for creating MCP applications.
55
"""
66

7+
import inspect
78
import warnings
89
from collections.abc import Callable
910
from typing import (
@@ -24,6 +25,7 @@
2425
from .cache import CacheBackend, ContextCache, MemoryCache
2526
from .context import EnrichContext
2627
from .entity import EnrichModel
28+
from .parameter import EnrichParameter
2729
from .relationship import Relationship
2830

2931
# Type variables
@@ -320,6 +322,52 @@ def describe_model(self) -> str:
320322

321323
return "\n".join(lines)
322324

325+
def _append_enrichparameter_hints(self, description: str, fn: Callable[..., Any]) -> str:
326+
"""Append ``EnrichParameter`` metadata to a description string."""
327+
328+
hints: list[str] = []
329+
try:
330+
sig = inspect.signature(fn)
331+
except (TypeError, ValueError): # pragma: no cover - defensive
332+
return description
333+
334+
for param in sig.parameters.values():
335+
default = param.default
336+
annotation = param.annotation
337+
338+
if isinstance(default, EnrichParameter):
339+
if annotation is EnrichContext:
340+
# Context parameters are stripped from the final tool
341+
# interface so hints would be confusing to the agent.
342+
continue
343+
344+
param_type = "Any"
345+
if annotation is not inspect.Parameter.empty:
346+
if get_origin(annotation) is Literal:
347+
values = ", ".join(repr(v) for v in get_args(annotation))
348+
param_type = f"Literal[{values}]"
349+
else:
350+
param_type = getattr(annotation, "__name__", str(annotation))
351+
352+
parts = [param_type]
353+
if default.description:
354+
parts.append(default.description)
355+
if default.examples:
356+
joined = ", ".join(map(str, default.examples))
357+
parts.append(f"examples: {joined}")
358+
if default.metadata:
359+
meta = ", ".join(f"{k}: {v}" for k, v in default.metadata.items())
360+
parts.append(meta)
361+
362+
hints.append(f"{param.name} - {'; '.join(parts)}")
363+
364+
if hints:
365+
description = (
366+
description.rstrip() + "\n\nParameter hints:\n" + "\n".join(f"- {h}" for h in hints)
367+
)
368+
369+
return description
370+
323371
def retrieve(
324372
self,
325373
func: Callable[..., Any] | None = None,
@@ -369,6 +417,9 @@ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
369417
if resource_desc == fn.__doc__ and resource_desc:
370418
resource_desc = resource_desc.strip()
371419

420+
# Append EnrichParameter parameter hints
421+
resource_desc = self._append_enrichparameter_hints(resource_desc, fn)
422+
372423
# Store the resource for testing
373424
self.resources[resource_name] = fn
374425
# Create and apply the MCP tool decorator

src/enrichmcp/parameter.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Utility for annotating function parameters with extra metadata."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from dataclasses import field as dataclass_field
7+
from typing import TYPE_CHECKING, Any
8+
9+
if TYPE_CHECKING: # pragma: no cover - import for type hints
10+
from collections.abc import Iterable
11+
12+
13+
@dataclass
14+
class EnrichParameter:
15+
"""Metadata container for function parameters.
16+
17+
When a parameter's default value is an instance of ``EnrichParameter`` the
18+
metadata contained here is appended to the generated tool description. The
19+
``default`` attribute is **not** used as the runtime default value; it simply
20+
provides a placeholder so function signatures remain valid.
21+
"""
22+
23+
default: Any | None = None
24+
description: str | None = None
25+
examples: Iterable[Any] | None = None
26+
metadata: dict[str, Any] = dataclass_field(default_factory=dict)
27+
28+
def __iter__(self) -> Iterable[Any]: # pragma: no cover - convenience
29+
yield self.default

tests/test_enrichparameter.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
5+
from enrichmcp import EnrichContext, EnrichMCP, EnrichParameter
6+
7+
8+
@pytest.mark.asyncio
9+
async def test_enrichparameter_hints_appended():
10+
app = EnrichMCP("Test", description="desc")
11+
with patch.object(app.mcp, "tool", wraps=app.mcp.tool) as mock_tool:
12+
13+
@app.retrieve(description="Base desc")
14+
async def my_resource(
15+
ctx: EnrichContext,
16+
name: str = EnrichParameter(description="user name", examples=["bob"]),
17+
) -> dict:
18+
return {}
19+
20+
desc = mock_tool.call_args.kwargs["description"]
21+
assert "Parameter hints:" in desc
22+
assert "name - str" in desc
23+
assert "user name" in desc
24+
assert "examples: bob" in desc
25+
assert "ctx" not in desc

0 commit comments

Comments
 (0)