Skip to content

Commit 543e86c

Browse files
author
Agasthya Kasturi
committed
added type validation for optional params
1 parent 397569a commit 543e86c

File tree

9 files changed

+1800
-695
lines changed

9 files changed

+1800
-695
lines changed

README.md

Lines changed: 289 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ The Model Context Protocol allows applications to provide context for LLMs in a
6666

6767
- Build MCP clients that can connect to any MCP server
6868
- Create MCP servers that expose resources, prompts and tools
69-
- Use standard transports like stdio and SSE
69+
- Use standard transports like stdio, SSE, and Streamable HTTP
7070
- Handle all MCP protocol messages and lifecycle events
7171

7272
## Installation
@@ -160,7 +160,7 @@ from dataclasses import dataclass
160160

161161
from fake_database import Database # Replace with your actual DB type
162162

163-
from mcp.server.fastmcp import Context, FastMCP
163+
from mcp.server.fastmcp import FastMCP
164164

165165
# Create a named server
166166
mcp = FastMCP("My App")
@@ -192,8 +192,9 @@ mcp = FastMCP("My App", lifespan=app_lifespan)
192192

193193
# Access type-safe lifespan context in tools
194194
@mcp.tool()
195-
def query_db(ctx: Context) -> str:
195+
def query_db() -> str:
196196
"""Tool that uses initialized resources"""
197+
ctx = mcp.get_context()
197198
db = ctx.request_context.lifespan_context["db"]
198199
return db.query()
199200
```
@@ -218,6 +219,15 @@ def get_config() -> str:
218219
def get_user_profile(user_id: str) -> str:
219220
"""Dynamic user data"""
220221
return f"Profile data for user {user_id}"
222+
223+
224+
# Example with form-style query expansion (RFC 6570) using multiple parameters
225+
@mcp.resource("articles://{article_id}/view{?format,lang}")
226+
def view_article(article_id: str, format: str = "html", lang: str = "english") -> str:
227+
"""View an article, with optional format and language selection.
228+
Example URI: articles://123/view?format=pdf&lang=english"""
229+
content = f"Content for article {article_id} in {format} format Viewing in {lang}."
230+
return content
221231
```
222232

223233
### Tools
@@ -309,6 +319,33 @@ async def long_task(files: list[str], ctx: Context) -> str:
309319
return "Processing complete"
310320
```
311321

322+
### Authentication
323+
324+
Authentication can be used by servers that want to expose tools accessing protected resources.
325+
326+
`mcp.server.auth` implements an OAuth 2.0 server interface, which servers can use by
327+
providing an implementation of the `OAuthServerProvider` protocol.
328+
329+
```
330+
mcp = FastMCP("My App",
331+
auth_server_provider=MyOAuthServerProvider(),
332+
auth=AuthSettings(
333+
issuer_url="https://myapp.com",
334+
revocation_options=RevocationOptions(
335+
enabled=True,
336+
),
337+
client_registration_options=ClientRegistrationOptions(
338+
enabled=True,
339+
valid_scopes=["myscope", "myotherscope"],
340+
default_scopes=["myscope"],
341+
),
342+
required_scopes=["myscope"],
343+
),
344+
)
345+
```
346+
347+
See [OAuthServerProvider](src/mcp/server/auth/provider.py) for more details.
348+
312349
## Running Your Server
313350

314351
### Development Mode
@@ -360,8 +397,95 @@ python server.py
360397
mcp run server.py
361398
```
362399

400+
Note that `mcp run` or `mcp dev` only supports server using FastMCP and not the low-level server variant.
401+
402+
### Streamable HTTP Transport
403+
404+
> **Note**: Streamable HTTP transport is superseding SSE transport for production deployments.
405+
406+
```python
407+
from mcp.server.fastmcp import FastMCP
408+
409+
# Stateful server (maintains session state)
410+
mcp = FastMCP("StatefulServer")
411+
412+
# Stateless server (no session persistence)
413+
mcp = FastMCP("StatelessServer", stateless_http=True)
414+
415+
# Stateless server (no session persistence, no sse stream with supported client)
416+
mcp = FastMCP("StatelessServer", stateless_http=True, json_response=True)
417+
418+
# Run server with streamable_http transport
419+
mcp.run(transport="streamable-http")
420+
```
421+
422+
You can mount multiple FastMCP servers in a FastAPI application:
423+
424+
```python
425+
# echo.py
426+
from mcp.server.fastmcp import FastMCP
427+
428+
mcp = FastMCP(name="EchoServer", stateless_http=True)
429+
430+
431+
@mcp.tool(description="A simple echo tool")
432+
def echo(message: str) -> str:
433+
return f"Echo: {message}"
434+
```
435+
436+
```python
437+
# math.py
438+
from mcp.server.fastmcp import FastMCP
439+
440+
mcp = FastMCP(name="MathServer", stateless_http=True)
441+
442+
443+
@mcp.tool(description="A simple add tool")
444+
def add_two(n: int) -> int:
445+
return n + 2
446+
```
447+
448+
```python
449+
# main.py
450+
import contextlib
451+
from fastapi import FastAPI
452+
from mcp.echo import echo
453+
from mcp.math import math
454+
455+
456+
# Create a combined lifespan to manage both session managers
457+
@contextlib.asynccontextmanager
458+
async def lifespan(app: FastAPI):
459+
async with contextlib.AsyncExitStack() as stack:
460+
await stack.enter_async_context(echo.mcp.session_manager.run())
461+
await stack.enter_async_context(math.mcp.session_manager.run())
462+
yield
463+
464+
465+
app = FastAPI(lifespan=lifespan)
466+
app.mount("/echo", echo.mcp.streamable_http_app())
467+
app.mount("/math", math.mcp.streamable_http_app())
468+
```
469+
470+
For low level server with Streamable HTTP implementations, see:
471+
- Stateful server: [`examples/servers/simple-streamablehttp/`](examples/servers/simple-streamablehttp/)
472+
- Stateless server: [`examples/servers/simple-streamablehttp-stateless/`](examples/servers/simple-streamablehttp-stateless/)
473+
474+
475+
476+
The streamable HTTP transport supports:
477+
- Stateful and stateless operation modes
478+
- Resumability with event stores
479+
- JSON or SSE response formats
480+
- Better scalability for multi-node deployments
481+
482+
363483
### Mounting to an Existing ASGI Server
364484

485+
> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).
486+
487+
By default, SSE servers are mounted at `/sse` and Streamable HTTP servers are mounted at `/mcp`. You can customize these paths using the methods described below.
488+
365489
You can mount the SSE server to an existing ASGI server using the `sse_app` method. This allows you to integrate the SSE server with other ASGI applications.
366490

367491
```python
@@ -383,6 +507,43 @@ app = Starlette(
383507
app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app()))
384508
```
385509

510+
When mounting multiple MCP servers under different paths, you can configure the mount path in several ways:
511+
512+
```python
513+
from starlette.applications import Starlette
514+
from starlette.routing import Mount
515+
from mcp.server.fastmcp import FastMCP
516+
517+
# Create multiple MCP servers
518+
github_mcp = FastMCP("GitHub API")
519+
browser_mcp = FastMCP("Browser")
520+
curl_mcp = FastMCP("Curl")
521+
search_mcp = FastMCP("Search")
522+
523+
# Method 1: Configure mount paths via settings (recommended for persistent configuration)
524+
github_mcp.settings.mount_path = "/github"
525+
browser_mcp.settings.mount_path = "/browser"
526+
527+
# Method 2: Pass mount path directly to sse_app (preferred for ad-hoc mounting)
528+
# This approach doesn't modify the server's settings permanently
529+
530+
# Create Starlette app with multiple mounted servers
531+
app = Starlette(
532+
routes=[
533+
# Using settings-based configuration
534+
Mount("/github", app=github_mcp.sse_app()),
535+
Mount("/browser", app=browser_mcp.sse_app()),
536+
# Using direct mount path parameter
537+
Mount("/curl", app=curl_mcp.sse_app("/curl")),
538+
Mount("/search", app=search_mcp.sse_app("/search")),
539+
]
540+
)
541+
542+
# Method 3: For direct execution, you can also pass the mount path to run()
543+
if __name__ == "__main__":
544+
search_mcp.run(transport="sse", mount_path="/search")
545+
```
546+
386547
For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes).
387548

388549
## Examples
@@ -403,6 +564,23 @@ def echo_resource(message: str) -> str:
403564
return f"Resource echo: {message}"
404565

405566

567+
# Example with form-style query expansion for customizing echo output
568+
@mcp.resource("echo://custom/{message}{?case,reverse}")
569+
def custom_echo_resource(
570+
message: str, case: str = "lower", reverse: bool = False
571+
) -> str:
572+
"""Echo a message with optional case transformation and reversal.
573+
Example URI: echo://custom/Hello?case=upper&reverse=true"""
574+
processed_message = message
575+
if case == "upper":
576+
processed_message = processed_message.upper()
577+
elif case == "lower":
578+
processed_message = processed_message.lower()
579+
if reverse:
580+
processed_message = processed_message[::-1]
581+
return f"Custom resource echo: {processed_message}"
582+
583+
406584
@mcp.tool()
407585
def echo_tool(message: str) -> str:
408586
"""Echo a message as a tool"""
@@ -435,6 +613,34 @@ def get_schema() -> str:
435613
return "\n".join(sql[0] for sql in schema if sql[0])
436614

437615

616+
# Example with form-style query expansion for table-specific schema
617+
@mcp.resource("schema://{table_name}{?include_indexes}")
618+
def get_table_schema(table_name: str, include_indexes: bool = False) -> str:
619+
"""Provide the schema for a specific table, optionally including indexes.
620+
Example URI: schema://users?include_indexes=true"""
621+
conn = sqlite3.connect("database.db")
622+
cursor = conn.cursor()
623+
try:
624+
base_query = "SELECT sql FROM sqlite_master WHERE type='table' AND name=?"
625+
params: list[str] = [table_name]
626+
if include_indexes:
627+
cursor.execute(base_query, params)
628+
schema_parts = cursor.fetchall()
629+
630+
index_query = (
631+
"SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name=?"
632+
)
633+
cursor.execute(index_query, params)
634+
schema_parts.extend(cursor.fetchall())
635+
else:
636+
cursor.execute(base_query, params)
637+
schema_parts = cursor.fetchall()
638+
639+
return "\n".join(sql[0] for sql in schema_parts if sql and sql[0])
640+
finally:
641+
conn.close()
642+
643+
438644
@mcp.tool()
439645
def query_data(sql: str) -> str:
440646
"""Execute SQL queries safely"""
@@ -555,9 +761,11 @@ if __name__ == "__main__":
555761
asyncio.run(run())
556762
```
557763

764+
Caution: The `mcp run` and `mcp dev` tool doesn't support low-level server.
765+
558766
### Writing MCP Clients
559767

560-
The SDK provides a high-level client interface for connecting to MCP servers:
768+
The SDK provides a high-level client interface for connecting to MCP servers using various [transports](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports):
561769

562770
```python
563771
from mcp import ClientSession, StdioServerParameters, types
@@ -621,6 +829,82 @@ if __name__ == "__main__":
621829
asyncio.run(run())
622830
```
623831

832+
Clients can also connect using [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http):
833+
834+
```python
835+
from mcp.client.streamable_http import streamablehttp_client
836+
from mcp import ClientSession
837+
838+
839+
async def main():
840+
# Connect to a streamable HTTP server
841+
async with streamablehttp_client("example/mcp") as (
842+
read_stream,
843+
write_stream,
844+
_,
845+
):
846+
# Create a session using the client streams
847+
async with ClientSession(read_stream, write_stream) as session:
848+
# Initialize the connection
849+
await session.initialize()
850+
# Call a tool
851+
tool_result = await session.call_tool("echo", {"message": "hello"})
852+
```
853+
854+
### OAuth Authentication for Clients
855+
856+
The SDK includes [authorization support](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for connecting to protected MCP servers:
857+
858+
```python
859+
from mcp.client.auth import OAuthClientProvider, TokenStorage
860+
from mcp.client.session import ClientSession
861+
from mcp.client.streamable_http import streamablehttp_client
862+
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
863+
864+
865+
class CustomTokenStorage(TokenStorage):
866+
"""Simple in-memory token storage implementation."""
867+
868+
async def get_tokens(self) -> OAuthToken | None:
869+
pass
870+
871+
async def set_tokens(self, tokens: OAuthToken) -> None:
872+
pass
873+
874+
async def get_client_info(self) -> OAuthClientInformationFull | None:
875+
pass
876+
877+
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
878+
pass
879+
880+
881+
async def main():
882+
# Set up OAuth authentication
883+
oauth_auth = OAuthClientProvider(
884+
server_url="https://api.example.com",
885+
client_metadata=OAuthClientMetadata(
886+
client_name="My Client",
887+
redirect_uris=["http://localhost:3000/callback"],
888+
grant_types=["authorization_code", "refresh_token"],
889+
response_types=["code"],
890+
),
891+
storage=CustomTokenStorage(),
892+
redirect_handler=lambda url: print(f"Visit: {url}"),
893+
callback_handler=lambda: ("auth_code", None),
894+
)
895+
896+
# Use with streamable HTTP client
897+
async with streamablehttp_client(
898+
"https://api.example.com/mcp", auth=oauth_auth
899+
) as (read, write, _):
900+
async with ClientSession(read, write) as session:
901+
await session.initialize()
902+
# Authenticated session ready
903+
```
904+
905+
For a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/).
906+
907+
624908
### MCP Primitives
625909

626910
The MCP protocol defines three core primitives that servers can implement:
@@ -655,4 +939,4 @@ We are passionate about supporting contributors of all levels of experience and
655939

656940
## License
657941

658-
This project is licensed under the MIT License - see the LICENSE file for details.
942+
This project is licensed under the MIT License - see the LICENSE file for details.

0 commit comments

Comments
 (0)