Skip to content

Commit 23bee43

Browse files
authored
Merge branch 'main' into drop-streamablehttp-client
2 parents b43491a + b26e5b9 commit 23bee43

File tree

17 files changed

+373
-31
lines changed

17 files changed

+373
-31
lines changed

examples/clients/simple-auth-client/README.md

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,63 +12,87 @@ A demonstration of how to use the MCP Python SDK with OAuth authentication over
1212

1313
```bash
1414
cd examples/clients/simple-auth-client
15-
uv sync --reinstall
15+
uv sync --reinstall
1616
```
1717

1818
## Usage
1919

2020
### 1. Start an MCP server with OAuth support
2121

22+
The simple-auth server example provides three server configurations. See [examples/servers/simple-auth/README.md](../../servers/simple-auth/README.md) for full details.
23+
24+
#### Option A: New Architecture (Recommended)
25+
26+
Separate Authorization Server and Resource Server:
27+
28+
```bash
29+
# Terminal 1: Start Authorization Server on port 9000
30+
cd examples/servers/simple-auth
31+
uv run mcp-simple-auth-as --port=9000
32+
33+
# Terminal 2: Start Resource Server on port 8001
34+
cd examples/servers/simple-auth
35+
uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http
36+
```
37+
38+
#### Option B: Legacy Server (Backwards Compatibility)
39+
2240
```bash
23-
# Example with mcp-simple-auth
24-
cd path/to/mcp-simple-auth
25-
uv run mcp-simple-auth --transport streamable-http --port 3001
41+
# Single server that acts as both AS and RS (port 8000)
42+
cd examples/servers/simple-auth
43+
uv run mcp-simple-auth-legacy --port=8000 --transport=streamable-http
2644
```
2745

2846
### 2. Run the client
2947

3048
```bash
31-
uv run mcp-simple-auth-client
49+
# Connect to Resource Server (new architecture, default port 8001)
50+
MCP_SERVER_PORT=8001 uv run mcp-simple-auth-client
3251

33-
# Or with custom server URL
34-
MCP_SERVER_PORT=3001 uv run mcp-simple-auth-client
52+
# Connect to Legacy Server (port 8000)
53+
uv run mcp-simple-auth-client
3554

3655
# Use SSE transport
37-
MCP_TRANSPORT_TYPE=sse uv run mcp-simple-auth-client
56+
MCP_SERVER_PORT=8001 MCP_TRANSPORT_TYPE=sse uv run mcp-simple-auth-client
3857
```
3958

4059
### 3. Complete OAuth flow
4160

4261
The client will open your browser for authentication. After completing OAuth, you can use commands:
4362

4463
- `list` - List available tools
45-
- `call <tool_name> [args]` - Call a tool with optional JSON arguments
64+
- `call <tool_name> [args]` - Call a tool with optional JSON arguments
4665
- `quit` - Exit
4766

4867
## Example
4968

5069
```markdown
51-
🔐 Simple MCP Auth Client
52-
Connecting to: http://localhost:3001
70+
🚀 Simple MCP Auth Client
71+
Connecting to: http://localhost:8001/mcp
72+
Transport type: streamable-http
5373

54-
Please visit the following URL to authorize the application:
55-
http://localhost:3001/authorize?response_type=code&client_id=...
74+
🔗 Attempting to connect to http://localhost:8001/mcp...
75+
📡 Opening StreamableHTTP transport connection with auth...
76+
Opening browser for authorization: http://localhost:9000/authorize?...
5677

57-
✅ Connected to MCP server at http://localhost:3001
78+
✅ Connected to MCP server at http://localhost:8001/mcp
5879

5980
mcp> list
6081
📋 Available tools:
61-
1. echo - Echo back the input text
82+
1. get_time
83+
Description: Get the current server time.
6284

63-
mcp> call echo {"text": "Hello, world!"}
64-
🔧 Tool 'echo' result:
65-
Hello, world!
85+
mcp> call get_time
86+
🔧 Tool 'get_time' result:
87+
{"current_time": "2024-01-15T10:30:00", "timezone": "UTC", ...}
6688

6789
mcp> quit
68-
👋 Goodbye!
6990
```
7091

7192
## Configuration
7293

73-
- `MCP_SERVER_PORT` - Server URL (default: 8000)
74-
- `MCP_TRANSPORT_TYPE` - Transport type: `streamable-http` (default) or `sse`
94+
| Environment Variable | Description | Default |
95+
|---------------------|-------------|---------|
96+
| `MCP_SERVER_PORT` | Port number of the MCP server | `8000` |
97+
| `MCP_TRANSPORT_TYPE` | Transport type: `streamable-http` or `sse` | `streamable-http` |
98+
| `MCP_CLIENT_METADATA_URL` | Optional URL for client metadata (CIMD) | None |

examples/servers/simple-auth/README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ uv run mcp-simple-auth-as --port=9000
3131
cd examples/servers/simple-auth
3232

3333
# Start Resource Server on port 8001, connected to Authorization Server
34-
uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http
34+
uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http
3535

3636
# With RFC 8707 strict resource validation (recommended for production)
37-
uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http --oauth-strict
37+
uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http --oauth-strict
3838

3939
```
4040

@@ -84,8 +84,9 @@ For backwards compatibility with older MCP implementations, a legacy server is p
8484
### Running the Legacy Server
8585

8686
```bash
87-
# Start legacy authorization server on port 8002
88-
uv run mcp-simple-auth-legacy --port=8002
87+
# Start legacy server on port 8000 (the default)
88+
cd examples/servers/simple-auth
89+
uv run mcp-simple-auth-legacy --port=8000 --transport=streamable-http
8990
```
9091

9192
**Differences from the new architecture:**
@@ -101,7 +102,7 @@ uv run mcp-simple-auth-legacy --port=8002
101102
```bash
102103
# Test with client (will automatically fall back to legacy discovery)
103104
cd examples/clients/simple-auth-client
104-
MCP_SERVER_PORT=8002 MCP_TRANSPORT_TYPE=streamable-http uv run mcp-simple-auth-client
105+
MCP_SERVER_PORT=8000 MCP_TRANSPORT_TYPE=streamable-http uv run mcp-simple-auth-client
105106
```
106107

107108
The client will:

src/mcp/server/fastmcp/resources/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Base classes and interfaces for FastMCP resources."""
22

33
import abc
4-
from typing import Annotated
4+
from typing import Annotated, Any
55

66
from pydantic import (
77
AnyUrl,
@@ -32,6 +32,7 @@ class Resource(BaseModel, abc.ABC):
3232
)
3333
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this resource")
3434
annotations: Annotations | None = Field(default=None, description="Optional annotations for the resource")
35+
meta: dict[str, Any] | None = Field(default=None, description="Optional metadata for this resource")
3536

3637
@field_validator("name", mode="before")
3738
@classmethod

src/mcp/server/fastmcp/resources/resource_manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def add_template(
6464
mime_type: str | None = None,
6565
icons: list[Icon] | None = None,
6666
annotations: Annotations | None = None,
67+
meta: dict[str, Any] | None = None,
6768
) -> ResourceTemplate:
6869
"""Add a template from a function."""
6970
template = ResourceTemplate.from_function(
@@ -75,6 +76,7 @@ def add_template(
7576
mime_type=mime_type,
7677
icons=icons,
7778
annotations=annotations,
79+
meta=meta,
7880
)
7981
self._templates[template.uri_template] = template
8082
return template

src/mcp/server/fastmcp/resources/templates.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class ResourceTemplate(BaseModel):
3030
mime_type: str = Field(default="text/plain", description="MIME type of the resource content")
3131
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for the resource template")
3232
annotations: Annotations | None = Field(default=None, description="Optional annotations for the resource template")
33+
meta: dict[str, Any] | None = Field(default=None, description="Optional metadata for this resource template")
3334
fn: Callable[..., Any] = Field(exclude=True)
3435
parameters: dict[str, Any] = Field(description="JSON schema for function parameters")
3536
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context")
@@ -45,6 +46,7 @@ def from_function(
4546
mime_type: str | None = None,
4647
icons: list[Icon] | None = None,
4748
annotations: Annotations | None = None,
49+
meta: dict[str, Any] | None = None,
4850
context_kwarg: str | None = None,
4951
) -> ResourceTemplate:
5052
"""Create a template from a function."""
@@ -74,6 +76,7 @@ def from_function(
7476
mime_type=mime_type or "text/plain",
7577
icons=icons,
7678
annotations=annotations,
79+
meta=meta,
7780
fn=fn,
7881
parameters=parameters,
7982
context_kwarg=context_kwarg,
@@ -112,6 +115,7 @@ async def create_resource(
112115
mime_type=self.mime_type,
113116
icons=self.icons,
114117
annotations=self.annotations,
118+
meta=self.meta,
115119
fn=lambda: result, # Capture result in closure
116120
)
117121
except Exception as e:

src/mcp/server/fastmcp/resources/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def from_function(
8383
mime_type: str | None = None,
8484
icons: list[Icon] | None = None,
8585
annotations: Annotations | None = None,
86+
meta: dict[str, Any] | None = None,
8687
) -> "FunctionResource":
8788
"""Create a FunctionResource from a function."""
8889
func_name = name or fn.__name__
@@ -101,6 +102,7 @@ def from_function(
101102
fn=fn,
102103
icons=icons,
103104
annotations=annotations,
105+
meta=meta,
104106
)
105107

106108

src/mcp/server/fastmcp/server.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ async def list_resources(self) -> list[MCPResource]:
376376
mimeType=resource.mime_type,
377377
icons=resource.icons,
378378
annotations=resource.annotations,
379+
_meta=resource.meta,
379380
)
380381
for resource in resources
381382
]
@@ -391,6 +392,7 @@ async def list_resource_templates(self) -> list[MCPResourceTemplate]:
391392
mimeType=template.mime_type,
392393
icons=template.icons,
393394
annotations=template.annotations,
395+
_meta=template.meta,
394396
)
395397
for template in templates
396398
]
@@ -405,7 +407,7 @@ async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContent
405407

406408
try:
407409
content = await resource.read()
408-
return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
410+
return [ReadResourceContents(content=content, mime_type=resource.mime_type, meta=resource.meta)]
409411
except Exception as e: # pragma: no cover
410412
logger.exception(f"Error reading resource {uri}")
411413
raise ResourceError(str(e))
@@ -557,6 +559,7 @@ def resource(
557559
mime_type: str | None = None,
558560
icons: list[Icon] | None = None,
559561
annotations: Annotations | None = None,
562+
meta: dict[str, Any] | None = None,
560563
) -> Callable[[AnyFunction], AnyFunction]:
561564
"""Decorator to register a function as a resource.
562565
@@ -575,6 +578,7 @@ def resource(
575578
title: Optional human-readable title for the resource
576579
description: Optional description of the resource
577580
mime_type: Optional MIME type for the resource
581+
meta: Optional metadata dictionary for the resource
578582
579583
Example:
580584
@server.resource("resource://my-resource")
@@ -633,6 +637,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
633637
mime_type=mime_type,
634638
icons=icons,
635639
annotations=annotations,
640+
meta=meta,
636641
)
637642
else:
638643
# Register as regular resource
@@ -645,6 +650,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
645650
mime_type=mime_type,
646651
icons=icons,
647652
annotations=annotations,
653+
meta=meta,
648654
)
649655
self.add_resource(resource)
650656
return fn

src/mcp/server/lowlevel/experimental.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
ServerResult,
3232
ServerTasksCapability,
3333
ServerTasksRequestsCapability,
34+
TasksCallCapability,
3435
TasksCancelCapability,
3536
TasksListCapability,
3637
TasksToolsCapability,
@@ -79,7 +80,7 @@ def update_capabilities(self, capabilities: ServerCapabilities) -> None:
7980
capabilities.tasks.cancel = TasksCancelCapability()
8081

8182
capabilities.tasks.requests = ServerTasksRequestsCapability(
82-
tools=TasksToolsCapability()
83+
tools=TasksToolsCapability(call=TasksCallCapability())
8384
) # assuming always supported for now
8485

8586
def enable_tasks(

src/mcp/server/lowlevel/helper_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from dataclasses import dataclass
2+
from typing import Any
23

34

45
@dataclass
@@ -7,3 +8,4 @@ class ReadResourceContents:
78

89
content: str | bytes
910
mime_type: str | None = None
11+
meta: dict[str, Any] | None = None

src/mcp/server/lowlevel/server.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,19 +344,23 @@ def decorator(
344344
async def handler(req: types.ReadResourceRequest):
345345
result = await func(req.params.uri)
346346

347-
def create_content(data: str | bytes, mime_type: str | None):
347+
def create_content(data: str | bytes, mime_type: str | None, meta: dict[str, Any] | None = None):
348+
# Note: ResourceContents uses Field(alias="_meta"), so we must use the alias key
349+
meta_kwargs: dict[str, Any] = {"_meta": meta} if meta is not None else {}
348350
match data:
349351
case str() as data:
350352
return types.TextResourceContents(
351353
uri=req.params.uri,
352354
text=data,
353355
mimeType=mime_type or "text/plain",
356+
**meta_kwargs,
354357
)
355358
case bytes() as data: # pragma: no cover
356359
return types.BlobResourceContents(
357360
uri=req.params.uri,
358361
blob=base64.b64encode(data).decode(),
359362
mimeType=mime_type or "application/octet-stream",
363+
**meta_kwargs,
360364
)
361365

362366
match result:
@@ -370,7 +374,10 @@ def create_content(data: str | bytes, mime_type: str | None):
370374
content = create_content(data, None)
371375
case Iterable() as contents:
372376
contents_list = [
373-
create_content(content_item.content, content_item.mime_type) for content_item in contents
377+
create_content(
378+
content_item.content, content_item.mime_type, getattr(content_item, "meta", None)
379+
)
380+
for content_item in contents
374381
]
375382
return types.ServerResult(
376383
types.ReadResourceResult(

0 commit comments

Comments
 (0)