Unlock AI-native potential: annotations, middleware, observability, streaming#21
Unlock AI-native potential: annotations, middleware, observability, streaming#21
Conversation
…treaming, constraints Wire disconnected systems and add missing MCP protocol features: - Add tool annotations (readOnlyHint, destructiveHint, etc.) to @cli.command - Route MCP tool/resource/prompt calls through middleware stack - Instrument MCP server with RequestLogger and milo://stats resource - Add Annotated type constraints (MinLen, MaxLen, Gt, Lt, Ge, Le, Pattern, Description) to schema generation - Stream Progress yields as MCP notifications/progress in real time - Add CLI.call_raw() for raw generator access - Add flagship deploy example demonstrating dual-mode commands - Rewrite README to lead with AI-native CLI story Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR expands Milo’s “AI-native CLI” surface by adding MCP tool annotations, deeper MCP↔middleware integration, request observability with a built-in stats resource, streaming progress notifications for generator-based commands, and richer JSON Schema generation via typing.Annotated.
Changes:
- Add MCP tool annotations support (emitted in
tools/listandllms.txt) and ship a dual-mode deploy example. - Wire CLI middleware into MCP handling (
tools/call,resources/read,prompts/get) and add streamingnotifications/progress. - Extend JSON Schema generation to unwrap
typing.Annotatedconstraints into schema keywords.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/milo/schema.py |
Adds Annotated constraint markers and applies them during schema generation. |
tests/test_schema_v2.py |
Adds coverage for Annotated-derived JSON Schema keywords. |
src/milo/commands.py |
Adds annotations= to @cli.command and introduces CLI.call_raw() for generator-aware calls. |
src/milo/_command_defs.py |
Persists annotations on command definitions (eager and lazy). |
src/milo/mcp.py |
Adds middleware routing for MCP calls, streaming progress notifications, and milo://stats resource. |
src/milo/_jsonrpc.py |
Adds JSON-RPC notification writer for progress streaming. |
src/milo/llms.py |
Renders MCP behavioral hints into llms.txt output. |
tests/test_ai_native.py |
Adds tests for annotations, middleware firing, stats resource, and streaming progress. |
tests/test_mcp_resources.py |
Updates resource listing expectations to include milo://stats. |
src/milo/__init__.py |
Exposes new schema markers via lazy imports and __all__. |
README.md |
Rewrites positioning and documentation to lead with AI-native / MCP capabilities. |
examples/deploy/app.py |
Adds a flagship dual-mode (human interactive + MCP structured) deploy example. |
examples/deploy/templates/confirm.kida |
Adds an interactive confirmation template used by the deploy example. |
Comments suppressed due to low confidence (2)
src/milo/schema.py:156
Description(...)metadata is intended to “override or supplement the parameter description”, but docstring-derived descriptions are applied unconditionally afterward, overwriting anyDescriptionset byAnnotated. Consider only applying docstring descriptions whendescriptionis not already present (or defining a clear precedence rule).
prop = _type_to_schema(annotation)
# Add description from docstring if available
if name in param_docs:
prop["description"] = param_docs[name]
tests/test_schema_v2.py:417
- For
Annotated[list[str], MinLen(1)], the expected JSON Schema keyword for arrays isminItems(notminLength). The current assertion will lock in an incorrect schema shape for arrays; update the test to assertminItems(and ensure the implementation emits it).
def func(tags: Annotated[list[str], MinLen(1)]):
pass
schema = function_to_schema(func)
prop = schema["properties"]["tags"]
assert prop["type"] == "array"
assert prop["minLength"] == 1
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _CONSTRAINT_MAP: dict[type, str] = { | ||
| MinLen: "minLength", | ||
| MaxLen: "maxLength", | ||
| Gt: "exclusiveMinimum", |
There was a problem hiding this comment.
MinLen/MaxLen are mapped to minLength/maxLength, but the docstring says they apply to strings/arrays. In JSON Schema, arrays use minItems/maxItems; using minLength on an array will be ignored by validators. Consider mapping based on the derived schema type (string vs array) or using separate markers, and update tests accordingly.
| def list_tools(self, params: dict[str, Any]) -> dict[str, Any]: | ||
| tools = self._cached_tools if self._cached_tools is not None else _list_tools(self._cli) | ||
| return {"tools": tools} | ||
|
|
There was a problem hiding this comment.
RequestLogger instrumentation is only applied to tools/call, resources/read, and prompts/get. tools/list, resources/list, and prompts/list are not logged/correlated, which doesn’t match the PR description of “instruments every call”. Either add similar timing/logging to the list endpoints or clarify scope (and ensure stats reflect the intended definition of “total”).
src/milo/mcp.py
Outdated
| return _stats_resource(self._logger) | ||
| new_correlation_id() | ||
| start = time.monotonic() | ||
| result = _read_resource(self._cli, params) |
There was a problem hiding this comment.
resources/read errors are currently swallowed inside _read_resource() and then logged here without an error value, so milo://stats will undercount errors for failing resource reads. Consider propagating exceptions to this layer (so you can log error), or have _read_resource() return an error indicator that can be fed into log_request(...).
| result = _read_resource(self._cli, params) | |
| try: | |
| result = _read_resource(self._cli, params) | |
| except Exception as e: | |
| log_request(self._logger, "resources/read", uri, start, error=str(e)) | |
| raise |
src/milo/mcp.py
Outdated
| new_correlation_id() | ||
| start = time.monotonic() | ||
| result = _get_prompt(self._cli, params) | ||
| log_request(self._logger, "prompts/get", params.get("name", ""), start) |
There was a problem hiding this comment.
Similar to resources/read, _get_prompt() converts exceptions into an "Error: ..." message payload, but log_request(...) is always called without an error value here. This will cause milo://stats error counts to miss prompt failures unless error information is propagated up for logging.
| log_request(self._logger, "prompts/get", params.get("name", ""), start) | |
| error = "" | |
| for message in result.get("messages", []): | |
| content = message.get("content", {}) | |
| text = content.get("text", "") if isinstance(content, dict) else "" | |
| if text.startswith("Error:"): | |
| error = text | |
| break | |
| log_request( | |
| self._logger, | |
| "prompts/get", | |
| params.get("name", ""), | |
| start, | |
| error=error, | |
| ) |
…ndpoints - MinLen/MaxLen now emit minItems/maxItems for array types instead of minLength/maxLength (correct per JSON Schema spec) - Add observability logging to list_tools, list_resources, list_prompts - Capture errors in read_resource and get_prompt logging - Update test to expect minItems for list types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
@cli.commandacceptsannotations={"destructiveHint": True, ...}per MCP spec, emitted intools/listandllms.txtMiddlewareStacknow fires ontools/call,resources/read, andprompts/get— not just CLI dispatchRequestLogger(correlation IDs, latency tracking) and exposesmilo://statsas a built-in resourcefunction_to_schemaunwrapstyping.AnnotatedwithMinLen,MaxLen,Gt,Lt,Ge,Le,Pattern,Descriptionmarkers into JSON Schema keywordsProgressobjects emitnotifications/progressJSON-RPC notifications to MCP clients in real time; addsCLI.call_raw()for raw generator accessexamples/deploy/demonstrates dual-mode commands — interactive confirmation UI for humans, structured JSON for AI agentsTest plan
ruff check src/milo/cleanexamples/deploy/app.py --llms-txtshows annotationsexamples/deploy/app.py --mcpwith streaming tool call emits progress notificationsmilo://statsresource returns latency data after tool calls (test included)🤖 Generated with Claude Code