added specs

This commit is contained in:
Zack Lee 2024-11-18 11:36:07 -05:00
commit 21a38ebe74
No known key found for this signature in database
3 changed files with 3532 additions and 60 deletions

View file

@ -1,8 +1,330 @@
---
title: "Event Handling"
description: "Handle events in your MCP server"
title: 'Event Handling'
description: 'Handle events and manage state in your MCP server'
---
# Event Handling
MCP servers process various events including requests, notifications, and connection lifecycle events. This guide explains how to implement event handlers effectively in your server.
Handle events in your MCP server
## Request handlers
Request handlers process incoming requests from clients and must return a response. Each handler is associated with a specific method name.
<CodeGroup>
```typescript server.ts
import { Server } from "@modelcontextprotocol/sdk/server";
import { ListToolsRequestSchema, ListToolsResultSchema } from "@modelcontextprotocol/sdk/types";
const server = new Server({
name: "example-server",
version: "1.0.0",
});
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
return {
tools: [
{
name: "example-tool",
description: "An example tool",
inputSchema: {
type: "object",
properties: {
message: { type: "string" },
},
},
},
],
};
});
```
```python server.py
from mcp import Server
from mcp.types import ListToolsRequest, ListToolsResult, Tool
server = Server(name="example-server", version="1.0.0")
@server.request_handler("tools/list")
async def handle_list_tools(request: ListToolsRequest) -> ListToolsResult:
return ListToolsResult(
tools=[
Tool(
name="example-tool",
description="An example tool",
input_schema={
"type": "object",
"properties": {
"message": {"type": "string"}
}
}
)
]
)
```
</CodeGroup>
## Notification handlers
Notification handlers process one-way messages that don't require responses. They're useful for handling events like resource updates or logging messages.
<CodeGroup>
```typescript notifications.ts
import { ResourceUpdatedNotificationSchema } from "@modelcontextprotocol/sdk/types";
server.setNotificationHandler(ResourceUpdatedNotificationSchema, async (notification) => {
const { uri } = notification.params;
console.log(`Resource updated: ${uri}`);
});
```
```python notifications.py
from mcp.types import ResourceUpdatedNotification
@server.notification_handler("notifications/resources/updated")
async def handle_resource_updated(notification: ResourceUpdatedNotification):
uri = notification.params.uri
print(f"Resource updated: {uri}")
```
</CodeGroup>
## Connection lifecycle
Your server can handle connection lifecycle events through callbacks. This is useful for setup and cleanup operations.
<CodeGroup>
```typescript lifecycle.ts
const server = new Server({
name: "example-server",
version: "1.0.0",
});
server.oninitialized = () => {
console.log("Client has completed initialization");
};
server.onclose = () => {
console.log("Connection closed");
// Perform cleanup
};
```
```python lifecycle.py
server = Server(name="example-server", version="1.0.0")
@server.on_initialized
async def handle_initialized():
print("Client has completed initialization")
@server.on_close
async def handle_close():
print("Connection closed")
# Perform cleanup
```
</CodeGroup>
## Error handling
Properly handle errors in your event handlers to provide meaningful feedback to clients.
<CodeGroup>
```typescript errors.ts
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types";
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
// Attempt to call tool
const result = await callTool(request.params);
return { toolResult: result };
} catch (error) {
throw new McpError(
ErrorCode.InvalidParams,
"Failed to execute tool",
{ details: error.message }
);
}
});
```
```python errors.py
from mcp.types import CallToolRequest, McpError, ErrorCode
@server.request_handler("tools/call")
async def handle_call_tool(request: CallToolRequest):
try:
# Attempt to call tool
result = await call_tool(request.params)
return {"toolResult": result}
except Exception as e:
raise McpError(
code=ErrorCode.INVALID_PARAMS,
message="Failed to execute tool",
data={"details": str(e)}
)
```
</CodeGroup>
## Progress reporting
For long-running operations, you can report progress back to the client.
<CodeGroup>
```typescript progress.ts
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const progressToken = request.params._meta?.progressToken;
if (progressToken) {
await server.notification({
method: "notifications/progress",
params: {
progressToken,
progress: 50,
total: 100
}
});
}
// Complete the operation
return { toolResult: "Operation completed" };
});
```
```python progress.py
@server.request_handler("tools/call")
async def handle_call_tool(request: CallToolRequest):
progress_token = request.params._meta.progress_token if request.params._meta else None
if progress_token:
await server.notify(
"notifications/progress",
{
"progressToken": progress_token,
"progress": 50,
"total": 100
}
)
# Complete the operation
return {"toolResult": "Operation completed"}
```
</CodeGroup>
## State management
While MCP servers are generally stateless, you might need to maintain some state between requests. Here's how to manage state safely:
<CodeGroup>
```typescript state.ts
class ExampleServer {
private subscriptions: Set<string> = new Set();
private server: Server;
constructor() {
this.server = new Server({
name: "example-server",
version: "1.0.0",
});
this.server.setRequestHandler(SubscribeRequestSchema, async (request) => {
const { uri } = request.params;
this.subscriptions.add(uri);
return {};
});
this.server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
const { uri } = request.params;
this.subscriptions.delete(uri);
return {};
});
this.server.onclose = () => {
this.subscriptions.clear();
};
}
}
```
```python state.py
class ExampleServer:
def __init__(self):
self.subscriptions = set()
self.server = Server(name="example-server", version="1.0.0")
@self.server.request_handler("resources/subscribe")
async def handle_subscribe(request: SubscribeRequest):
self.subscriptions.add(request.params.uri)
return {}
@self.server.request_handler("resources/unsubscribe")
async def handle_unsubscribe(request: UnsubscribeRequest):
self.subscriptions.remove(request.params.uri)
return {}
@self.server.on_close
async def handle_close():
self.subscriptions.clear()
```
</CodeGroup>
## Best practices
When implementing event handlers in your MCP server:
<CardGroup cols={2}>
<Card title="Validation" icon="shield-check">
Always validate request parameters before processing
</Card>
<Card title="Error handling" icon="triangle-exclamation">
Use appropriate error codes and provide helpful error messages
</Card>
<Card title="Async operations" icon="spinner">
Handle long-running operations asynchronously and report progress
</Card>
<Card title="Resource cleanup" icon="broom">
Clean up resources and state when connections are closed
</Card>
</CardGroup>
## Debugging tips
To help debug your event handlers:
1. Enable logging in your server:
<CodeGroup>
```typescript logging.ts
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
const { level } = request.params;
server.notification({
method: "notifications/message",
params: {
level: "debug",
data: `Setting log level to: ${level}`
}
});
return {};
});
```
```python logging.py
@server.request_handler("logging/setLevel")
async def handle_set_level(request: SetLevelRequest):
level = request.params.level
await server.notify(
"notifications/message",
{
"level": "debug",
"data": f"Setting log level to: {level}"
}
)
return {}
```
</CodeGroup>
2. Use the progress reporting mechanism to debug long-running operations
3. Implement comprehensive error handling with detailed error messages
4. Add request/response logging for debugging complex interactions
Remember that logging and debugging information should be appropriate for the execution environment and should never expose sensitive information.

View file

@ -1,8 +1,385 @@
---
title: "Resource Management"
description: "Manage resources in your MCP server"
title: 'Resource Management'
description: 'How to implement resource management in your MCP server'
---
# Resource Management
Resources in MCP are any type of content that can be loaded as context for an LLM. This guide explains how to implement resource management in your MCP server.
Manage resources in your MCP server
## Overview
Resources are identified by URIs and can represent:
- Files on the local filesystem
- Remote data (e.g., database records, API responses)
- Generated content
- Dynamic system state
## Basic resource implementation
To implement resources in your MCP server, you need to handle two main requests:
- `resources/list`: Lists available resources
- `resources/read`: Reads the content of a specific resource
<CodeGroup>
```typescript server.ts
import { Server } from "@modelcontextprotocol/sdk/server";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema
} from "@modelcontextprotocol/sdk/types";
const server = new Server({
name: "example-server",
version: "1.0.0",
});
// Handle resource listing
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "file:///example.txt",
name: "Example File",
mimeType: "text/plain",
description: "An example text file resource"
}
]
};
});
// Handle resource reading
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
// Read and return the resource content
return {
contents: [
{
uri: uri,
mimeType: "text/plain",
text: "Example content"
}
]
};
});
```
```python server.py
from mcp import Server
from mcp.types import Resource, ResourceContents
server = Server(
name="example-server",
version="1.0.0"
)
# Handle resource listing
@server.request_handler("resources/list")
async def list_resources(request):
return {
"resources": [
{
"uri": "file:///example.txt",
"name": "Example File",
"mimeType": "text/plain",
"description": "An example text file resource"
}
]
}
# Handle resource reading
@server.request_handler("resources/read")
async def read_resource(request):
uri = request.params["uri"]
# Read and return the resource content
return {
"contents": [
{
"uri": uri,
"mimeType": "text/plain",
"text": "Example content"
}
]
}
```
</CodeGroup>
## Resource content types
MCP supports two types of resource contents:
- Text content: For plain text and other text-based formats
- Binary content: For images and other binary data (encoded as base64)
<CodeGroup>
```typescript content-types.ts
// Text content example
const textContent = {
uri: "file:///document.txt",
mimeType: "text/plain",
text: "This is the text content"
};
// Binary content example
const binaryContent = {
uri: "file:///image.png",
mimeType: "image/png",
blob: Buffer.from("...").toString("base64")
};
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
return {
contents: [
// Return either text or binary content based on the resource
request.params.uri.endsWith(".txt") ? textContent : binaryContent
]
};
});
```
```python content-types.py
import base64
# Text content example
text_content = {
"uri": "file:///document.txt",
"mimeType": "text/plain",
"text": "This is the text content"
}
# Binary content example
binary_content = {
"uri": "file:///image.png",
"mimeType": "image/png",
"blob": base64.b64encode(b"...").decode()
}
@server.request_handler("resources/read")
async def read_resource(request):
return {
"contents": [
# Return either text or binary content based on the resource
text_content if request.params["uri"].endswith(".txt") else binary_content
]
}
```
</CodeGroup>
## Resource templates
Resource templates allow servers to define patterns for dynamic resources using URI templates (RFC 6570).
<CodeGroup>
```typescript templates.ts
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
// Static resources
{
uri: "file:///static.txt",
name: "Static File"
}
],
resourceTemplates: [
// Dynamic resources
{
uriTemplate: "user://{userId}/profile",
name: "User Profile",
description: "Access user profile data",
mimeType: "application/json"
}
]
};
});
```
```python templates.py
@server.request_handler("resources/list")
async def list_resources(request):
return {
"resources": [
# Static resources
{
"uri": "file:///static.txt",
"name": "Static File"
}
],
"resourceTemplates": [
# Dynamic resources
{
"uriTemplate": "user://{userId}/profile",
"name": "User Profile",
"description": "Access user profile data",
"mimeType": "application/json"
}
]
}
```
</CodeGroup>
## Resource updates
MCP supports notifying clients about resource changes through subscriptions and notifications.
<CodeGroup>
```typescript updates.ts
import { SubscribeRequestSchema, UnsubscribeRequestSchema } from "@modelcontextprotocol/sdk/types";
// Track subscribed resources
const subscriptions = new Set<string>();
// Handle subscription requests
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
const uri = request.params.uri;
subscriptions.add(uri);
return {};
});
// Handle unsubscribe requests
server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
const uri = request.params.uri;
subscriptions.delete(uri);
return {};
});
// Notify about resource updates
function notifyResourceUpdate(uri: string) {
if (subscriptions.has(uri)) {
server.notification({
method: "notifications/resources/updated",
params: { uri }
});
}
}
```
```python updates.py
# Track subscribed resources
subscriptions = set()
# Handle subscription requests
@server.request_handler("resources/subscribe")
async def subscribe(request):
uri = request.params["uri"]
subscriptions.add(uri)
return {}
# Handle unsubscribe requests
@server.request_handler("resources/unsubscribe")
async def unsubscribe(request):
uri = request.params["uri"]
subscriptions.remove(uri)
return {}
# Notify about resource updates
async def notify_resource_update(uri: str):
if uri in subscriptions:
await server.notification(
method="notifications/resources/updated",
params={"uri": uri}
)
```
</CodeGroup>
## Pagination
For servers with many resources, MCP supports pagination using cursors.
<CodeGroup>
```typescript pagination.ts
const PAGE_SIZE = 10;
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
const cursor = request.params?.cursor;
let startIndex = 0;
if (cursor) {
startIndex = parseInt(Buffer.from(cursor, 'base64').toString());
}
const resources = getAllResources();
const pageResources = resources.slice(startIndex, startIndex + PAGE_SIZE);
// Calculate next cursor if there are more resources
const nextCursor = startIndex + PAGE_SIZE < resources.length
? Buffer.from(String(startIndex + PAGE_SIZE)).toString('base64')
: undefined;
return {
resources: pageResources,
nextCursor
};
});
```
```python pagination.py
PAGE_SIZE = 10
@server.request_handler("resources/list")
async def list_resources(request):
cursor = request.params.get("cursor")
start_index = 0
if cursor:
start_index = int(base64.b64decode(cursor))
resources = get_all_resources()
page_resources = resources[start_index:start_index + PAGE_SIZE]
# Calculate next cursor if there are more resources
next_cursor = (base64.b64encode(str(start_index + PAGE_SIZE).encode()).decode()
if start_index + PAGE_SIZE < len(resources) else None)
return {
"resources": page_resources,
"nextCursor": next_cursor
}
```
</CodeGroup>
## Best practices
When implementing resource management:
<CardGroup cols={2}>
<Card title="URI design" icon="link">
Use meaningful URI schemes and hierarchical paths for resource organization
</Card>
<Card title="Error handling" icon="triangle-exclamation">
Return appropriate error codes for missing or inaccessible resources
</Card>
<Card title="Content types" icon="file-code">
Always specify accurate MIME types for resource contents
</Card>
<Card title="Resource cleanup" icon="broom">
Implement proper cleanup for subscriptions when connections close
</Card>
</CardGroup>
## Resource security
When exposing resources:
- Validate all URI parameters and resource paths
- Implement appropriate access controls
- Sanitize and validate all resource content
- Consider rate limiting for resource-intensive operations
- Use secure transport mechanisms (e.g., HTTPS) for remote resources
## Common pitfalls
- Failing to handle URI encoding/decoding properly
- Not cleaning up subscriptions when connections close
- Returning inconsistent MIME types
- Missing error handling for invalid resource requests
- Not implementing pagination for large resource collections
- Memory leaks from unmanaged resource subscriptions

View file

@ -1,8 +1,462 @@
---
title: "Tool Implementation"
description: "Implement tools in your MCP server"
title: 'Tool Implementation'
description: 'Learn how to implement tools in your MCP server'
---
# Tool Implementation
Tools are one of the core features of MCP, allowing servers to expose functionality that can be invoked by clients or AI models. This guide explains how to implement tools in your MCP server.
Implement tools in your MCP server
## Tool definition
Tools are defined by implementing the `Tool` interface, which describes the tool's name, description, and input schema.
<CodeGroup>
```typescript Tool interface in TypeScript
interface Tool {
// The name of the tool
name: string;
// A human-readable description
description?: string;
// JSON Schema defining the expected parameters
inputSchema: {
type: "object";
properties?: Record<string, unknown>;
};
}
```
```python Tool interface in Python
@dataclass
class Tool:
# The name of the tool
name: str
# A human-readable description
description: Optional[str] = None
# JSON Schema defining the expected parameters
input_schema: Dict[str, Any] = field(default_factory=lambda: {
"type": "object",
"properties": {}
})
```
</CodeGroup>
## Basic implementation
Here's a simple example of implementing a tool that adds two numbers:
<CodeGroup>
```typescript calculator.ts
import { Server } from "@modelcontextprotocol/sdk/server";
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types";
const server = new Server({
name: "calculator-server",
version: "1.0.0",
});
// Define and register available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "add",
description: "Add two numbers together",
inputSchema: {
type: "object",
properties: {
a: { type: "number", description: "First number" },
b: { type: "number", description: "Second number" }
},
required: ["a", "b"]
}
}
]
};
});
// Implement tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "add") {
const a = request.params.arguments?.a as number;
const b = request.params.arguments?.b as number;
return {
toolResult: a + b
};
}
throw new Error("Tool not found");
});
```
```python calculator.py
from dataclasses import dataclass
from typing import Dict, Any
from mcp_sdk import Server, Tool
server = Server(
name="calculator-server",
version="1.0.0"
)
# Define the tool
add_tool = Tool(
name="add",
description="Add two numbers together",
input_schema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Second number"}
},
"required": ["a", "b"]
}
)
@server.handle_list_tools
async def handle_list_tools(request):
return {
"tools": [add_tool]
}
@server.handle_call_tool
async def handle_call_tool(request):
if request.params.name == "add":
a = request.params.arguments["a"]
b = request.params.arguments["b"]
return {
"toolResult": a + b
}
raise ValueError("Tool not found")
```
</CodeGroup>
## Progress reporting
For long-running operations, tools can report progress back to the client:
<CodeGroup>
```typescript long-operation.ts
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "longOperation",
description: "A time-consuming operation with progress updates",
inputSchema: {
type: "object",
properties: {
duration: {
type: "number",
description: "Duration in seconds"
}
}
}
}
]
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "longOperation") {
const duration = request.params.arguments?.duration as number ?? 10;
const steps = 10;
const stepDuration = duration / steps;
const progressToken = request.params._meta?.progressToken;
for (let i = 1; i <= steps; i++) {
await new Promise(resolve => setTimeout(resolve, stepDuration * 1000));
if (progressToken) {
server.notification({
method: "notifications/progress",
params: {
progress: i,
total: steps,
progressToken
}
});
}
}
return { toolResult: "Operation completed" };
}
throw new Error("Tool not found");
});
```
```python long_operation.py
import asyncio
from mcp_sdk import Server, Tool
long_operation_tool = Tool(
name="longOperation",
description="A time-consuming operation with progress updates",
input_schema={
"type": "object",
"properties": {
"duration": {
"type": "number",
"description": "Duration in seconds"
}
}
}
)
@server.handle_list_tools
async def handle_list_tools(request):
return {
"tools": [long_operation_tool]
}
@server.handle_call_tool
async def handle_call_tool(request):
if request.params.name == "longOperation":
duration = request.params.arguments.get("duration", 10)
steps = 10
step_duration = duration / steps
progress_token = request.params._meta.get("progressToken")
for i in range(1, steps + 1):
await asyncio.sleep(step_duration)
if progress_token:
await server.notification({
"method": "notifications/progress",
"params": {
"progress": i,
"total": steps,
"progressToken": progress_token
}
})
return {"toolResult": "Operation completed"}
raise ValueError("Tool not found")
```
</CodeGroup>
## Error handling
Tools should handle errors gracefully and return appropriate error information:
<CodeGroup>
```typescript error-handling.ts
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "riskyOperation") {
try {
// Attempt the operation
const result = await performRiskyOperation(request.params.arguments);
return { toolResult: result };
} catch (error) {
if (error instanceof ValidationError) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid parameters provided",
error.details
);
}
if (error instanceof OperationError) {
throw new McpError(
ErrorCode.InternalError,
"Operation failed",
{
reason: error.message,
retryable: error.isRetryable
}
);
}
throw error;
}
}
});
```
```python error_handling.py
from mcp_sdk import McpError, ErrorCode
@server.handle_call_tool
async def handle_call_tool(request):
if request.params.name == "riskyOperation":
try:
# Attempt the operation
result = await perform_risky_operation(request.params.arguments)
return {"toolResult": result}
except ValidationError as e:
raise McpError(
code=ErrorCode.INVALID_PARAMS,
message="Invalid parameters provided",
data=e.details
)
except OperationError as e:
raise McpError(
code=ErrorCode.INTERNAL_ERROR,
message="Operation failed",
data={
"reason": str(e),
"retryable": e.is_retryable
}
)
```
</CodeGroup>
## Best practices
When implementing tools, follow these guidelines:
<CardGroup cols={2}>
<Card title="Clear descriptions" icon="book">
Provide detailed descriptions that help AI models understand when and how to use the tool
</Card>
<Card title="Input validation" icon="shield-check">
Use JSON Schema to define and validate input parameters thoroughly
</Card>
<Card title="Progress updates" icon="bars-progress">
Send regular progress updates for operations that take more than a few seconds
</Card>
<Card title="Error handling" icon="triangle-exclamation">
Return specific error codes and helpful error messages
</Card>
</CardGroup>
### Tool naming conventions
- Use camelCase for tool names in TypeScript/JavaScript
- Use snake_case for tool names in Python
- Keep names concise but descriptive
- Use verbs for action-oriented tools (e.g., `convertCurrency`, `fetchWeather`)
- Use nouns for data-oriented tools (e.g., `userProfile`, `systemStatus`)
### Input schema best practices
- Be explicit about required vs optional parameters
- Include descriptions for all parameters
- Use appropriate JSON Schema types and formats
- Consider adding validation constraints (min, max, pattern, etc.)
Example of a well-defined input schema:
<CodeGroup>
```typescript schema-example.ts
const toolSchema = {
type: "object",
properties: {
query: {
type: "string",
description: "Search query string",
minLength: 1,
maxLength: 100
},
limit: {
type: "integer",
description: "Maximum number of results",
minimum: 1,
maximum: 100,
default: 10
},
includeArchived: {
type: "boolean",
description: "Include archived items in results",
default: false
}
},
required: ["query"]
};
```
```python schema_example.py
tool_schema = {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query string",
"minLength": 1,
"maxLength": 100
},
"limit": {
"type": "integer",
"description": "Maximum number of results",
"minimum": 1,
"maximum": 100,
"default": 10
},
"include_archived": {
"type": "boolean",
"description": "Include archived items in results",
"default": False
}
},
"required": ["query"]
}
```
</CodeGroup>
## Testing tools
Here's how to test your tool implementations:
<CodeGroup>
```typescript tool-test.ts
import { TestServer } from "@modelcontextprotocol/sdk/testing";
describe("Calculator tools", () => {
let server: TestServer;
beforeEach(() => {
server = new TestServer();
// Initialize your tools
});
it("should add two numbers", async () => {
const response = await server.callTool("add", {
a: 5,
b: 3
});
expect(response.toolResult).toBe(8);
});
it("should handle invalid inputs", async () => {
await expect(
server.callTool("add", {
a: "not a number",
b: 3
})
).rejects.toThrow();
});
});
```
```python tool_test.py
import pytest
from mcp_sdk.testing import TestServer
@pytest.fixture
async def server():
server = TestServer()
# Initialize your tools
return server
async def test_add_numbers(server):
response = await server.call_tool("add", {
"a": 5,
"b": 3
})
assert response.tool_result == 8
async def test_invalid_input(server):
with pytest.raises(ValueError):
await server.call_tool("add", {
"a": "not a number",
"b": 3
})
```
</CodeGroup>