Source code for duck.contrib.mcp.testserver

"""
MCP protocol feature test server.

This module provides a complete test MCP server implementation for validating
Duck's MCP support, including:

- Tool registration and execution
- Tool authorization using scopes
- Server-to-client notifications
- Progress notifications
- Client sampling requests
- Client root discovery
- User elicitation requests
- Resource listing and reading
- Resource templates
- Prompt generation
- MCP session handling
- SSE streaming transport

The server is intended for development and protocol verification only.
It exposes `/mcp` as an MCP endpoint and can be connected to MCP clients
such as MCP Inspector for testing supported features.

Example:
    Connect an MCP client to:

        http://localhost:<port>/mcp

    Authentication testing:
        Authorization: Bearer admin-token

    The `admin_test` tool requires the `admin` scope.

"""

import asyncio

from duck.urls import path
from duck.contrib.mcp import MCPView, tool, resource, prompt, resource_template


[docs] class TestMCPServer(MCPView): """ MCP test server covering the main protocol features. """ name = "duck-mcp-test-server" version = "1.0.0" sse = True
[docs] async def authenticate(self): """ Test authentication provider. In a real server this would validate a token/session. """ from duck.contrib.mcp.auth import AuthResult token = self.request.get_header("authorization") if token == "Bearer admin-token": return AuthResult(scopes={"admin"}) return AuthResult(scopes=[]) # Authenticated but with no granted scopes. Use AuthResult.deny() to reject authentication entirely.
[docs] @tool(description="Send a custom MCP notification") async def test_notification(self, message: str) -> str: """ Test sending custom notifications to the client. This verifies that the active SSE stream receives server push messages. """ await self.notify( "notifications/test", { "message": message, "timestamp": asyncio.get_event_loop().time(), }, ) return "Notification sent"
[docs] @tool(description="Admin-only test tool", scopes=["admin"]) async def admin_test(self, message: str) -> dict: """ Test scoped authorization. Requires the client/user to have the `admin` scope. """ return { "message": message, "authorized": True, "scope": "admin", }
[docs] @tool(description="Add two numbers") async def add(self, a: int, b: int) -> int: """ Test addition tool. """ return a + b
[docs] @tool(description="Ask client for sampling generation") async def test_sampling(self, prompt: str): """ Test sampling/createMessage. """ response = await self.capabilities.sampling.create_message( messages=[ { "role": "user", "content": { "type": "text", "text": prompt, }, } ], max_tokens=100, ) return response
[docs] @tool(description="Test client root discovery") async def test_roots(self): """ Test roots/list. """ roots = await self.capabilities.roots.list_roots() return { "roots": roots, }
[docs] @tool(description="Ask the user a question") async def test_elicitation(self): """ Test elicitation/create. """ result = await self.capabilities.elicitation.create( message="What is your favorite programming language?", requested_schema={ "type": "object", "properties": { "language": { "type": "string", } }, "required": [ "language", ], }, ) return result
[docs] @resource( uri="test://hello", name="hello", description="Simple test resource", mime_type="text/plain", ) async def hello_resource(self): """ Test resource. """ return "Hello from MCP resource"
[docs] @resource_template( uri_template="users:///{user_id}", name="User Profile", description="Returns a user profile by ID", mime_type="application/json", ) async def user_profile(self, user_id: str): return { "id": user_id, "name": "Test User", }
[docs] @prompt( name="test_prompt", description="Generate a test prompt", ) async def test_prompt(self, name: str): """ Test prompt generation. """ return [ { "role": "user", "content": { "type": "text", "text": f"Hello {name}", }, } ]