added specs
This commit is contained in:
parent
9681ec94e6
commit
21a38ebe74
3 changed files with 3532 additions and 60 deletions
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue