Skip to content

Commit da4fce2

Browse files
Add everything-server for comprehensive MCP conformance testing (#1587)
1 parent 9eae96a commit da4fce2

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)