Skip to content

Unlock AI-native potential: annotations, middleware, observability, streaming#21

Merged
lbliii merged 4 commits intomainfrom
lbliii/distill-milo-genius
Apr 3, 2026
Merged

Unlock AI-native potential: annotations, middleware, observability, streaming#21
lbliii merged 4 commits intomainfrom
lbliii/distill-milo-genius

Conversation

@lbliii
Copy link
Copy Markdown
Owner

@lbliii lbliii commented Apr 3, 2026

Summary

  • MCP tool annotations: @cli.command accepts annotations={"destructiveHint": True, ...} per MCP spec, emitted in tools/list and llms.txt
  • Middleware wired into MCP: MiddlewareStack now fires on tools/call, resources/read, and prompts/get — not just CLI dispatch
  • Observability: MCP server instruments every call with RequestLogger (correlation IDs, latency tracking) and exposes milo://stats as a built-in resource
  • Annotated type constraints: function_to_schema unwraps typing.Annotated with MinLen, MaxLen, Gt, Lt, Ge, Le, Pattern, Description markers into JSON Schema keywords
  • Streaming MCP results: Commands yielding Progress objects emit notifications/progress JSON-RPC notifications to MCP clients in real time; adds CLI.call_raw() for raw generator access
  • Flagship deploy example: examples/deploy/ demonstrates dual-mode commands — interactive confirmation UI for humans, structured JSON for AI agents
  • README rewrite: Repositioned to lead with "Build CLIs that humans and AI agents both use natively" instead of the Elm TUI story

Test plan

  • All 898 tests pass (878 original + 20 new)
  • ruff check src/milo/ clean
  • Verify examples/deploy/app.py --llms-txt shows annotations
  • Verify examples/deploy/app.py --mcp with streaming tool call emits progress notifications
  • Verify middleware fires on MCP tool calls (test included)
  • Verify milo://stats resource returns latency data after tool calls (test included)

🤖 Generated with Claude Code

lbliii and others added 3 commits April 3, 2026 15:57
…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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/list and llms.txt) and ship a dual-mode deploy example.
  • Wire CLI middleware into MCP handling (tools/call, resources/read, prompts/get) and add streaming notifications/progress.
  • Extend JSON Schema generation to unwrap typing.Annotated constraints 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 any Description set by Annotated. Consider only applying docstring descriptions when description is 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 is minItems (not minLength). The current assertion will lock in an incorrect schema shape for arrays; update the test to assert minItems (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.

Comment on lines +75 to +78
_CONSTRAINT_MAP: dict[type, str] = {
MinLen: "minLength",
MaxLen: "maxLength",
Gt: "exclusiveMinimum",
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 47 to 50
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}

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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”).

Copilot uses AI. Check for mistakes.
src/milo/mcp.py Outdated
return _stats_resource(self._logger)
new_correlation_id()
start = time.monotonic()
result = _read_resource(self._cli, params)
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(...).

Suggested change
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

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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,
)

Copilot uses AI. Check for mistakes.
…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>
@lbliii lbliii merged commit 0502b19 into main Apr 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants