Skip to content

Commit 02b4187

Browse files
Add everything-server for comprehensive MCP conformance testing
Implements a reference server that exercises all MCP protocol features including tools, resources, prompts, logging, completion, sampling, and elicitation. This server is used to validate the Python SDK against the official MCP conformance test suite. The implementation uses FastMCP for ergonomics and passes all 25 conformance scenarios. It matches the functionality of the TypeScript everything-server to ensure consistency across language implementations.
1 parent b7b0f8e commit 02b4187

File tree

6 files changed

+469
-0
lines changed

6 files changed

+469
-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 mcp_everything_server.server import main
4+
5+
if __name__ == "__main__":
6+
main()
Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
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 logging
10+
from typing import Any
11+
12+
import click
13+
from mcp.server.fastmcp import Context, FastMCP
14+
from mcp.server.fastmcp.prompts.base import UserMessage
15+
from mcp.server.session import ServerSession
16+
from mcp.types import (
17+
AudioContent,
18+
CompletionArgument,
19+
CompletionContext,
20+
EmbeddedResource,
21+
ImageContent,
22+
PromptReference,
23+
ResourceTemplateReference,
24+
SamplingMessage,
25+
TextContent,
26+
TextResourceContents,
27+
)
28+
from pydantic import BaseModel, Field
29+
30+
# Configure logging
31+
logger = logging.getLogger(__name__)
32+
33+
# ===== TEST DATA =====
34+
35+
# Sample base64 encoded 1x1 red PNG pixel for testing
36+
TEST_IMAGE_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="
37+
38+
# Sample base64 encoded minimal WAV file for testing
39+
TEST_AUDIO_BASE64 = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA="
40+
41+
# ===== SERVER STATE =====
42+
43+
# Resource subscriptions tracking
44+
resource_subscriptions: set[str] = set()
45+
watched_resource_content = "Watched resource content"
46+
47+
# ===== CREATE FASTMCP SERVER =====
48+
49+
mcp = FastMCP(
50+
name="mcp-conformance-test-server",
51+
)
52+
53+
54+
# ===== TOOLS =====
55+
56+
57+
@mcp.tool()
58+
def test_simple_text() -> str:
59+
"""Tests simple text content response"""
60+
return "This is a simple text response for testing."
61+
62+
63+
@mcp.tool()
64+
def test_image_content() -> list[ImageContent]:
65+
"""Tests image content response"""
66+
return [ImageContent(type="image", data=TEST_IMAGE_BASE64, mimeType="image/png")]
67+
68+
69+
@mcp.tool()
70+
def test_audio_content() -> list[AudioContent]:
71+
"""Tests audio content response"""
72+
return [AudioContent(type="audio", data=TEST_AUDIO_BASE64, mimeType="audio/wav")]
73+
74+
75+
@mcp.tool()
76+
def test_embedded_resource() -> list[EmbeddedResource]:
77+
"""Tests embedded resource content response"""
78+
return [
79+
EmbeddedResource(
80+
type="resource",
81+
resource=TextResourceContents(
82+
uri="test://embedded-resource",
83+
mimeType="text/plain",
84+
text="This is an embedded resource content.",
85+
),
86+
)
87+
]
88+
89+
90+
@mcp.tool()
91+
def test_multiple_content_types() -> list[TextContent | ImageContent | EmbeddedResource]:
92+
"""Tests response with multiple content types (text, image, resource)"""
93+
return [
94+
TextContent(type="text", text="Multiple content types test:"),
95+
ImageContent(type="image", data=TEST_IMAGE_BASE64, mimeType="image/png"),
96+
EmbeddedResource(
97+
type="resource",
98+
resource=TextResourceContents(
99+
uri="test://mixed-content-resource",
100+
mimeType="application/json",
101+
text='{"test": "data", "value": 123}',
102+
),
103+
),
104+
]
105+
106+
107+
@mcp.tool()
108+
async def test_tool_with_logging(ctx: Context) -> str:
109+
"""Tests tool that emits log messages during execution"""
110+
await ctx.info("Tool execution started")
111+
await asyncio.sleep(0.05)
112+
113+
await ctx.info("Tool processing data")
114+
await asyncio.sleep(0.05)
115+
116+
await ctx.info("Tool execution completed")
117+
return "Tool with logging executed successfully"
118+
119+
120+
@mcp.tool()
121+
async def test_tool_with_progress(ctx: Context) -> str:
122+
"""Tests tool that reports progress notifications"""
123+
await ctx.report_progress(progress=0, total=100, message="Completed step 0 of 100")
124+
await asyncio.sleep(0.05)
125+
126+
await ctx.report_progress(progress=50, total=100, message="Completed step 50 of 100")
127+
await asyncio.sleep(0.05)
128+
129+
await ctx.report_progress(progress=100, total=100, message="Completed step 100 of 100")
130+
131+
# Return progress token as string
132+
progress_token = ctx.request_context.meta.progressToken if ctx.request_context and ctx.request_context.meta else 0
133+
return str(progress_token)
134+
135+
136+
@mcp.tool()
137+
def test_error_handling() -> str:
138+
"""Tests error response handling"""
139+
raise RuntimeError("This tool intentionally returns an error for testing")
140+
141+
142+
class SamplingInput(BaseModel):
143+
prompt: str = Field(description="The prompt to send to the LLM")
144+
145+
146+
@mcp.tool()
147+
async def test_sampling(prompt: str, ctx: Context[ServerSession, None]) -> str:
148+
"""Tests server-initiated sampling (LLM completion request)"""
149+
try:
150+
# Request sampling from client
151+
result = await ctx.session.create_message(
152+
messages=[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
153+
max_tokens=100,
154+
)
155+
156+
if result.content.type == "text":
157+
model_response = result.content.text
158+
else:
159+
model_response = "No response"
160+
161+
return f"LLM response: {model_response}"
162+
except Exception as e:
163+
return f"Sampling not supported or error: {str(e)}"
164+
165+
166+
class ElicitationInput(BaseModel):
167+
message: str = Field(description="The message to show the user")
168+
169+
170+
class UserResponse(BaseModel):
171+
response: str = Field(description="User's response")
172+
173+
174+
@mcp.tool()
175+
async def test_elicitation(message: str, ctx: Context[ServerSession, None]) -> str:
176+
"""Tests server-initiated elicitation (user input request)"""
177+
try:
178+
# Request user input from client
179+
result = await ctx.elicit(message=message, schema=UserResponse)
180+
181+
return (
182+
f"User response: action={result.action}, content={result.data.model_dump_json() if result.data else '{}'}"
183+
)
184+
except Exception as e:
185+
return f"Elicitation not supported or error: {str(e)}"
186+
187+
188+
# ===== RESOURCES =====
189+
190+
191+
@mcp.resource("test://static-text")
192+
def static_text_resource() -> str:
193+
"""A static text resource for testing"""
194+
return "This is the content of the static text resource."
195+
196+
197+
@mcp.resource("test://static-binary")
198+
def static_binary_resource() -> bytes:
199+
"""A static binary resource (image) for testing"""
200+
import base64
201+
202+
return base64.b64decode(TEST_IMAGE_BASE64)
203+
204+
205+
@mcp.resource("test://template/{id}/data")
206+
def template_resource(id: str) -> str:
207+
"""A resource template with parameter substitution"""
208+
import json
209+
210+
return json.dumps({"id": id, "templateTest": True, "data": f"Data for ID: {id}"})
211+
212+
213+
@mcp.resource("test://watched-resource")
214+
def watched_resource() -> str:
215+
"""A resource that can be subscribed to for updates"""
216+
return watched_resource_content
217+
218+
219+
# ===== PROMPTS =====
220+
221+
222+
@mcp.prompt()
223+
def test_simple_prompt() -> list[UserMessage]:
224+
"""A simple prompt without arguments"""
225+
return [UserMessage(role="user", content=TextContent(type="text", text="This is a simple prompt for testing."))]
226+
227+
228+
@mcp.prompt()
229+
def test_prompt_with_arguments(arg1: str, arg2: str) -> list[UserMessage]:
230+
"""A prompt with required arguments"""
231+
return [
232+
UserMessage(
233+
role="user", content=TextContent(type="text", text=f"Prompt with arguments: arg1='{arg1}', arg2='{arg2}'")
234+
)
235+
]
236+
237+
238+
@mcp.prompt()
239+
def test_prompt_with_embedded_resource(resourceUri: str) -> list[UserMessage]:
240+
"""A prompt that includes an embedded resource"""
241+
return [
242+
UserMessage(
243+
role="user",
244+
content=EmbeddedResource(
245+
type="resource",
246+
resource=TextResourceContents(
247+
uri=resourceUri,
248+
mimeType="text/plain",
249+
text="Embedded resource content for testing.",
250+
),
251+
),
252+
),
253+
UserMessage(role="user", content=TextContent(type="text", text="Please process the embedded resource above.")),
254+
]
255+
256+
257+
@mcp.prompt()
258+
def test_prompt_with_image() -> list[UserMessage]:
259+
"""A prompt that includes image content"""
260+
return [
261+
UserMessage(role="user", content=ImageContent(type="image", data=TEST_IMAGE_BASE64, mimeType="image/png")),
262+
UserMessage(role="user", content=TextContent(type="text", text="Please analyze the image above.")),
263+
]
264+
265+
266+
# ===== CUSTOM REQUEST HANDLERS =====
267+
268+
269+
def setup_custom_handlers():
270+
"""Set up custom request handlers for logging and completion."""
271+
272+
# Handler for logging/setLevel
273+
@mcp._mcp_server.set_logging_level()
274+
async def handle_set_logging_level(level: str) -> None:
275+
"""Handle logging level changes"""
276+
logger.info(f"Log level set to: {level}")
277+
# In a real implementation, you would adjust the logging level here
278+
# For conformance testing, we just acknowledge the request
279+
280+
# Handler for resources/subscribe
281+
async def handle_subscribe(uri: str) -> dict[str, Any]:
282+
"""Handle resource subscription"""
283+
resource_subscriptions.add(uri)
284+
logger.info(f"Subscribed to resource: {uri}")
285+
return {}
286+
287+
# Handler for resources/unsubscribe
288+
async def handle_unsubscribe(uri: str) -> dict[str, Any]:
289+
"""Handle resource unsubscription"""
290+
resource_subscriptions.discard(uri)
291+
logger.info(f"Unsubscribed from resource: {uri}")
292+
return {}
293+
294+
# Register subscription handlers
295+
mcp._mcp_server.subscribe_resource()(handle_subscribe)
296+
mcp._mcp_server.unsubscribe_resource()(handle_unsubscribe)
297+
298+
# Handler for completion/complete
299+
@mcp._mcp_server.completion()
300+
async def handle_completion(
301+
ref: PromptReference | ResourceTemplateReference,
302+
argument: CompletionArgument,
303+
context: CompletionContext | None,
304+
) -> dict[str, Any]:
305+
"""Handle completion requests"""
306+
# Basic completion support - returns empty array for conformance
307+
# Real implementations would provide contextual suggestions
308+
return {"values": [], "total": 0, "hasMore": False}
309+
310+
311+
# Set up custom handlers when module is loaded
312+
setup_custom_handlers()
313+
314+
315+
# ===== CLI =====
316+
317+
318+
@click.command()
319+
@click.option("--port", default=3001, help="Port to listen on for HTTP")
320+
@click.option(
321+
"--log-level",
322+
default="INFO",
323+
help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
324+
)
325+
def main(port: int, log_level: str) -> int:
326+
"""Run the MCP Everything Server."""
327+
# Configure logging
328+
logging.basicConfig(
329+
level=getattr(logging, log_level.upper()),
330+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
331+
)
332+
333+
logger.info(f"Starting MCP Everything Server on port {port}")
334+
logger.info(f"Endpoint will be: http://localhost:{port}/mcp")
335+
336+
# Update settings and run
337+
mcp.settings.port = port
338+
mcp.run(transport="streamable-http")
339+
340+
return 0
341+
342+
343+
if __name__ == "__main__":
344+
main()

0 commit comments

Comments
 (0)