Skip to content

Commit a2f030e

Browse files
Add everything-server for MCP conformance testing
Implements a comprehensive reference server that exercises all MCP protocol features for conformance testing. This server validates that the Python SDK correctly implements the MCP specification by passing all 25 conformance test scenarios. Features implemented: - 11 tools covering all content types (text, image, audio, embedded resources, mixed content, logging, progress, error handling, sampling, elicitation) - 4 resources (static text/binary, templates, subscribable resources) - 4 prompts with various content types - Logging support (logging/setLevel) - Completion support (completion/complete) - Resource subscriptions (resources/subscribe, resources/unsubscribe) - Session management with StreamableHTTP transport Implementation uses FastMCP with proper type safety: - All URIs wrapped with AnyUrl() for type correctness - Context properly typed with ServerSession parameter - Subscribe/unsubscribe handlers use correct AnyUrl parameter and None return - Completion handler returns Completion objects - ElicitationResult uses discriminated union narrowing via action field - Relative imports in __main__.py to avoid type stub warnings Type checking: Passes pyright with 0 errors (only 3 targeted ignores for accessing _mcp_server private API, documented with TODO to add public APIs to FastMCP in the future). Conformance: Passes all 25 test scenarios.
1 parent b7b0f8e commit a2f030e

File tree

6 files changed

+434
-0
lines changed

6 files changed

+434
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# MCP Everything Server
2+
3+
A comprehensive MCP server implementing all protocol features for conformance testing.
4+
5+
## Overview
6+
7+
The Everything Server is a reference implementation that demonstrates all features of the Model Context Protocol (MCP). It is designed to be used with the [MCP Conformance Test Framework](https://github.com/modelcontextprotocol/conformance) to validate MCP client and server implementations.
8+
9+
## Installation
10+
11+
From the python-sdk root directory:
12+
13+
```bash
14+
uv sync --frozen
15+
```
16+
17+
## Usage
18+
19+
### Running the Server
20+
21+
Start the server with default settings (port 3001):
22+
23+
```bash
24+
uv run -m mcp_everything_server
25+
```
26+
27+
Or with custom options:
28+
29+
```bash
30+
uv run -m mcp_everything_server --port 3001 --log-level DEBUG
31+
```
32+
33+
The server will be available at: `http://localhost:3001/mcp`
34+
35+
### Command-Line Options
36+
37+
- `--port` - Port to listen on (default: 3001)
38+
- `--log-level` - Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL (default: INFO)
39+
40+
## Running Conformance Tests
41+
42+
See the [MCP Conformance Test Framework](https://github.com/modelcontextprotocol/conformance) for instructions on running conformance tests against this server.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""MCP Everything Server - Comprehensive conformance test server."""
2+
3+
__version__ = "0.1.0"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""CLI entry point for the MCP Everything Server."""
2+
3+
from .server import main
4+
5+
if __name__ == "__main__":
6+
main()
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
#!/usr/bin/env python3
2+
"""
3+
MCP Everything Server - Conformance Test Server
4+
5+
Server implementing all MCP features for conformance testing based on Conformance Server Specification.
6+
"""
7+
8+
import asyncio
9+
import base64
10+
import json
11+
import logging
12+
13+
import click
14+
from mcp.server.fastmcp import Context, FastMCP
15+
from mcp.server.fastmcp.prompts.base import UserMessage
16+
from mcp.server.session import ServerSession
17+
from mcp.types import (
18+
AudioContent,
19+
Completion,
20+
CompletionArgument,
21+
CompletionContext,
22+
EmbeddedResource,
23+
ImageContent,
24+
PromptReference,
25+
ResourceTemplateReference,
26+
SamplingMessage,
27+
TextContent,
28+
TextResourceContents,
29+
)
30+
from pydantic import AnyUrl, BaseModel, Field
31+
32+
logger = logging.getLogger(__name__)
33+
34+
# Test data
35+
TEST_IMAGE_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="
36+
TEST_AUDIO_BASE64 = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA="
37+
38+
# Server state
39+
resource_subscriptions: set[str] = set()
40+
watched_resource_content = "Watched resource content"
41+
42+
mcp = FastMCP(
43+
name="mcp-conformance-test-server",
44+
)
45+
46+
47+
# Tools
48+
@mcp.tool()
49+
def test_simple_text() -> str:
50+
"""Tests simple text content response"""
51+
return "This is a simple text response for testing."
52+
53+
54+
@mcp.tool()
55+
def test_image_content() -> list[ImageContent]:
56+
"""Tests image content response"""
57+
return [ImageContent(type="image", data=TEST_IMAGE_BASE64, mimeType="image/png")]
58+
59+
60+
@mcp.tool()
61+
def test_audio_content() -> list[AudioContent]:
62+
"""Tests audio content response"""
63+
return [AudioContent(type="audio", data=TEST_AUDIO_BASE64, mimeType="audio/wav")]
64+
65+
66+
@mcp.tool()
67+
def test_embedded_resource() -> list[EmbeddedResource]:
68+
"""Tests embedded resource content response"""
69+
return [
70+
EmbeddedResource(
71+
type="resource",
72+
resource=TextResourceContents(
73+
uri=AnyUrl("test://embedded-resource"),
74+
mimeType="text/plain",
75+
text="This is an embedded resource content.",
76+
),
77+
)
78+
]
79+
80+
81+
@mcp.tool()
82+
def test_multiple_content_types() -> list[TextContent | ImageContent | EmbeddedResource]:
83+
"""Tests response with multiple content types (text, image, resource)"""
84+
return [
85+
TextContent(type="text", text="Multiple content types test:"),
86+
ImageContent(type="image", data=TEST_IMAGE_BASE64, mimeType="image/png"),
87+
EmbeddedResource(
88+
type="resource",
89+
resource=TextResourceContents(
90+
uri=AnyUrl("test://mixed-content-resource"),
91+
mimeType="application/json",
92+
text='{"test": "data", "value": 123}',
93+
),
94+
),
95+
]
96+
97+
98+
@mcp.tool()
99+
async def test_tool_with_logging(ctx: Context[ServerSession, None]) -> str:
100+
"""Tests tool that emits log messages during execution"""
101+
await ctx.info("Tool execution started")
102+
await asyncio.sleep(0.05)
103+
104+
await ctx.info("Tool processing data")
105+
await asyncio.sleep(0.05)
106+
107+
await ctx.info("Tool execution completed")
108+
return "Tool with logging executed successfully"
109+
110+
111+
@mcp.tool()
112+
async def test_tool_with_progress(ctx: Context[ServerSession, None]) -> str:
113+
"""Tests tool that reports progress notifications"""
114+
await ctx.report_progress(progress=0, total=100, message="Completed step 0 of 100")
115+
await asyncio.sleep(0.05)
116+
117+
await ctx.report_progress(progress=50, total=100, message="Completed step 50 of 100")
118+
await asyncio.sleep(0.05)
119+
120+
await ctx.report_progress(progress=100, total=100, message="Completed step 100 of 100")
121+
122+
# Return progress token as string
123+
progress_token = ctx.request_context.meta.progressToken if ctx.request_context and ctx.request_context.meta else 0
124+
return str(progress_token)
125+
126+
127+
@mcp.tool()
128+
async def test_sampling(prompt: str, ctx: Context[ServerSession, None]) -> str:
129+
"""Tests server-initiated sampling (LLM completion request)"""
130+
try:
131+
# Request sampling from client
132+
result = await ctx.session.create_message(
133+
messages=[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
134+
max_tokens=100,
135+
)
136+
137+
if result.content.type == "text":
138+
model_response = result.content.text
139+
else:
140+
model_response = "No response"
141+
142+
return f"LLM response: {model_response}"
143+
except Exception as e:
144+
return f"Sampling not supported or error: {str(e)}"
145+
146+
147+
class UserResponse(BaseModel):
148+
response: str = Field(description="User's response")
149+
150+
151+
@mcp.tool()
152+
async def test_elicitation(message: str, ctx: Context[ServerSession, None]) -> str:
153+
"""Tests server-initiated elicitation (user input request)"""
154+
try:
155+
# Request user input from client
156+
result = await ctx.elicit(message=message, schema=UserResponse)
157+
158+
# Type-safe discriminated union narrowing using action field
159+
if result.action == "accept":
160+
content = result.data.model_dump_json()
161+
else: # decline or cancel
162+
content = "{}"
163+
164+
return f"User response: action={result.action}, content={content}"
165+
except Exception as e:
166+
return f"Elicitation not supported or error: {str(e)}"
167+
168+
169+
@mcp.tool()
170+
def test_error_handling() -> str:
171+
"""Tests error response handling"""
172+
raise RuntimeError("This tool intentionally returns an error for testing")
173+
174+
175+
# Resources
176+
@mcp.resource("test://static-text")
177+
def static_text_resource() -> str:
178+
"""A static text resource for testing"""
179+
return "This is the content of the static text resource."
180+
181+
182+
@mcp.resource("test://static-binary")
183+
def static_binary_resource() -> bytes:
184+
"""A static binary resource (image) for testing"""
185+
return base64.b64decode(TEST_IMAGE_BASE64)
186+
187+
188+
@mcp.resource("test://template/{id}/data")
189+
def template_resource(id: str) -> str:
190+
"""A resource template with parameter substitution"""
191+
return json.dumps({"id": id, "templateTest": True, "data": f"Data for ID: {id}"})
192+
193+
194+
@mcp.resource("test://watched-resource")
195+
def watched_resource() -> str:
196+
"""A resource that can be subscribed to for updates"""
197+
return watched_resource_content
198+
199+
200+
# Prompts
201+
@mcp.prompt()
202+
def test_simple_prompt() -> list[UserMessage]:
203+
"""A simple prompt without arguments"""
204+
return [UserMessage(role="user", content=TextContent(type="text", text="This is a simple prompt for testing."))]
205+
206+
207+
@mcp.prompt()
208+
def test_prompt_with_arguments(arg1: str, arg2: str) -> list[UserMessage]:
209+
"""A prompt with required arguments"""
210+
return [
211+
UserMessage(
212+
role="user", content=TextContent(type="text", text=f"Prompt with arguments: arg1='{arg1}', arg2='{arg2}'")
213+
)
214+
]
215+
216+
217+
@mcp.prompt()
218+
def test_prompt_with_embedded_resource(resourceUri: str) -> list[UserMessage]:
219+
"""A prompt that includes an embedded resource"""
220+
return [
221+
UserMessage(
222+
role="user",
223+
content=EmbeddedResource(
224+
type="resource",
225+
resource=TextResourceContents(
226+
uri=AnyUrl(resourceUri),
227+
mimeType="text/plain",
228+
text="Embedded resource content for testing.",
229+
),
230+
),
231+
),
232+
UserMessage(role="user", content=TextContent(type="text", text="Please process the embedded resource above.")),
233+
]
234+
235+
236+
@mcp.prompt()
237+
def test_prompt_with_image() -> list[UserMessage]:
238+
"""A prompt that includes image content"""
239+
return [
240+
UserMessage(role="user", content=ImageContent(type="image", data=TEST_IMAGE_BASE64, mimeType="image/png")),
241+
UserMessage(role="user", content=TextContent(type="text", text="Please analyze the image above.")),
242+
]
243+
244+
245+
# Custom request handlers
246+
# TODO(felix): Add public APIs to FastMCP for subscribe_resource, unsubscribe_resource,
247+
# and set_logging_level to avoid accessing protected _mcp_server attribute.
248+
@mcp._mcp_server.set_logging_level() # pyright: ignore[reportPrivateUsage]
249+
async def handle_set_logging_level(level: str) -> None:
250+
"""Handle logging level changes"""
251+
logger.info(f"Log level set to: {level}")
252+
# In a real implementation, you would adjust the logging level here
253+
# For conformance testing, we just acknowledge the request
254+
255+
256+
async def handle_subscribe(uri: AnyUrl) -> None:
257+
"""Handle resource subscription"""
258+
resource_subscriptions.add(str(uri))
259+
logger.info(f"Subscribed to resource: {uri}")
260+
261+
262+
async def handle_unsubscribe(uri: AnyUrl) -> None:
263+
"""Handle resource unsubscription"""
264+
resource_subscriptions.discard(str(uri))
265+
logger.info(f"Unsubscribed from resource: {uri}")
266+
267+
268+
mcp._mcp_server.subscribe_resource()(handle_subscribe) # pyright: ignore[reportPrivateUsage]
269+
mcp._mcp_server.unsubscribe_resource()(handle_unsubscribe) # pyright: ignore[reportPrivateUsage]
270+
271+
272+
@mcp.completion()
273+
async def _handle_completion(
274+
ref: PromptReference | ResourceTemplateReference,
275+
argument: CompletionArgument,
276+
context: CompletionContext | None,
277+
) -> Completion:
278+
"""Handle completion requests"""
279+
# Basic completion support - returns empty array for conformance
280+
# Real implementations would provide contextual suggestions
281+
return Completion(values=[], total=0, hasMore=False)
282+
283+
284+
# CLI
285+
@click.command()
286+
@click.option("--port", default=3001, help="Port to listen on for HTTP")
287+
@click.option(
288+
"--log-level",
289+
default="INFO",
290+
help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
291+
)
292+
def main(port: int, log_level: str) -> int:
293+
"""Run the MCP Everything Server."""
294+
logging.basicConfig(
295+
level=getattr(logging, log_level.upper()),
296+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
297+
)
298+
299+
logger.info(f"Starting MCP Everything Server on port {port}")
300+
logger.info(f"Endpoint will be: http://localhost:{port}/mcp")
301+
302+
mcp.settings.port = port
303+
mcp.run(transport="streamable-http")
304+
305+
return 0
306+
307+
308+
if __name__ == "__main__":
309+
main()

0 commit comments

Comments
 (0)