Remove \archived-contributions
This commit is contained in:
parent
979bac32d9
commit
b829c5eed7
@ -1,35 +0,0 @@
|
|||||||
id: java-mcp-development
|
|
||||||
name: Java MCP Server Development
|
|
||||||
description: 'Complete toolkit for building Model Context Protocol servers in Java using the official MCP Java SDK with reactive streams and Spring Boot integration.'
|
|
||||||
tags: [java, mcp, model-context-protocol, server-development, sdk, reactive-streams, spring-boot, reactor]
|
|
||||||
items:
|
|
||||||
- path: instructions/java-mcp-server.instructions.md
|
|
||||||
kind: instruction
|
|
||||||
- path: prompts/java-mcp-server-generator.prompt.md
|
|
||||||
kind: prompt
|
|
||||||
- path: chatmodes/java-mcp-expert.chatmode.md
|
|
||||||
kind: chat-mode
|
|
||||||
usage: |
|
|
||||||
recommended
|
|
||||||
|
|
||||||
This chat mode provides expert guidance for building MCP servers in Java.
|
|
||||||
|
|
||||||
This chat mode is ideal for:
|
|
||||||
- Creating new MCP server projects with Java
|
|
||||||
- Implementing reactive handlers with Project Reactor
|
|
||||||
- Setting up stdio or HTTP transports
|
|
||||||
- Debugging reactive streams and error handling
|
|
||||||
- Learning Java MCP best practices with the official SDK
|
|
||||||
- Integrating with Spring Boot applications
|
|
||||||
|
|
||||||
To get the best results, consider:
|
|
||||||
- Using the instruction file to set context for Java MCP development
|
|
||||||
- Using the prompt to generate initial project structure
|
|
||||||
- Switching to the expert chat mode for detailed implementation help
|
|
||||||
- Specifying whether you need Maven or Gradle
|
|
||||||
- Providing details about what tools or functionality you need
|
|
||||||
- Mentioning if you need Spring Boot integration
|
|
||||||
|
|
||||||
display:
|
|
||||||
ordering: manual
|
|
||||||
show_badge: true
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
# Java MCP Server Development
|
|
||||||
|
|
||||||
'Complete toolkit for building Model Context Protocol servers in Java using the official MCP Java SDK with reactive streams and Spring Boot integration.'
|
|
||||||
|
|
||||||
**Tags:** java, mcp, model-context-protocol, server-development, sdk, reactive-streams, spring-boot, reactor
|
|
||||||
|
|
||||||
## Items in this Collection
|
|
||||||
|
|
||||||
| Title | Type | Description |
|
|
||||||
| ----- | ---- | ----------- |
|
|
||||||
| [Java MCP Server Development Guidelines](../instructions/java-mcp-server.instructions.md)<br />[](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-mcp-server.instructions.md)<br />[](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-mcp-server.instructions.md) | Instruction | Best practices and patterns for building Model Context Protocol (MCP) servers in Java using the official MCP Java SDK with reactive streams and Spring integration. |
|
|
||||||
| [Java MCP Server Generator](../prompts/java-mcp-server-generator.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fjava-mcp-server-generator.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode-insiders%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fjava-mcp-server-generator.prompt.md) | Prompt | Generate a complete Model Context Protocol server project in Java using the official MCP Java SDK with reactive streams and optional Spring Boot integration. |
|
|
||||||
| [Java MCP Expert](../chatmodes/java-mcp-expert.chatmode.md)<br />[](https://aka.ms/awesome-copilot/install/chatmode?url=vscode%3Achat-mode%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fchatmodes%2Fjava-mcp-expert.chatmode.md)<br />[](https://aka.ms/awesome-copilot/install/chatmode?url=vscode-insiders%3Achat-mode%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fchatmodes%2Fjava-mcp-expert.chatmode.md) | Chat Mode | Expert assistance for building Model Context Protocol servers in Java using reactive streams, the official MCP Java SDK, and Spring Boot integration. [see usage](#java-mcp-expert) |
|
|
||||||
|
|
||||||
## Collection Usage
|
|
||||||
|
|
||||||
### Java MCP Expert
|
|
||||||
|
|
||||||
recommended
|
|
||||||
|
|
||||||
This chat mode provides expert guidance for building MCP servers in Java.
|
|
||||||
|
|
||||||
This chat mode is ideal for:
|
|
||||||
- Creating new MCP server projects with Java
|
|
||||||
- Implementing reactive handlers with Project Reactor
|
|
||||||
- Setting up stdio or HTTP transports
|
|
||||||
- Debugging reactive streams and error handling
|
|
||||||
- Learning Java MCP best practices with the official SDK
|
|
||||||
- Integrating with Spring Boot applications
|
|
||||||
|
|
||||||
To get the best results, consider:
|
|
||||||
- Using the instruction file to set context for Java MCP development
|
|
||||||
- Using the prompt to generate initial project structure
|
|
||||||
- Switching to the expert chat mode for detailed implementation help
|
|
||||||
- Specifying whether you need Maven or Gradle
|
|
||||||
- Providing details about what tools or functionality you need
|
|
||||||
- Mentioning if you need Spring Boot integration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*This collection includes 3 curated items for java mcp server development.*
|
|
||||||
@ -1,325 +0,0 @@
|
|||||||
---
|
|
||||||
description: 'Expert assistance for building Model Context Protocol servers in Java using reactive streams, the official MCP Java SDK, and Spring Boot integration.'
|
|
||||||
model: GPT-4.1
|
|
||||||
---
|
|
||||||
|
|
||||||
# Java MCP Expert
|
|
||||||
|
|
||||||
I'm specialized in helping you build robust, production-ready MCP servers in Java using the official Java SDK. I can assist with:
|
|
||||||
|
|
||||||
## Core Capabilities
|
|
||||||
|
|
||||||
### Server Architecture
|
|
||||||
- Setting up McpServer with builder pattern
|
|
||||||
- Configuring capabilities (tools, resources, prompts)
|
|
||||||
- Implementing stdio and HTTP transports
|
|
||||||
- Reactive Streams with Project Reactor
|
|
||||||
- Synchronous facade for blocking use cases
|
|
||||||
- Spring Boot integration with starters
|
|
||||||
|
|
||||||
### Tool Development
|
|
||||||
- Creating tool definitions with JSON schemas
|
|
||||||
- Implementing tool handlers with Mono/Flux
|
|
||||||
- Parameter validation and error handling
|
|
||||||
- Async tool execution with reactive pipelines
|
|
||||||
- Tool list changed notifications
|
|
||||||
|
|
||||||
### Resource Management
|
|
||||||
- Defining resource URIs and metadata
|
|
||||||
- Implementing resource read handlers
|
|
||||||
- Managing resource subscriptions
|
|
||||||
- Resource changed notifications
|
|
||||||
- Multi-content responses (text, image, binary)
|
|
||||||
|
|
||||||
### Prompt Engineering
|
|
||||||
- Creating prompt templates with arguments
|
|
||||||
- Implementing prompt get handlers
|
|
||||||
- Multi-turn conversation patterns
|
|
||||||
- Dynamic prompt generation
|
|
||||||
- Prompt list changed notifications
|
|
||||||
|
|
||||||
### Reactive Programming
|
|
||||||
- Project Reactor operators and pipelines
|
|
||||||
- Mono for single results, Flux for streams
|
|
||||||
- Error handling in reactive chains
|
|
||||||
- Context propagation for observability
|
|
||||||
- Backpressure management
|
|
||||||
|
|
||||||
## Code Assistance
|
|
||||||
|
|
||||||
I can help you with:
|
|
||||||
|
|
||||||
### Maven Dependencies
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
|
||||||
<artifactId>mcp</artifactId>
|
|
||||||
<version>0.14.1</version>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Server Creation
|
|
||||||
```java
|
|
||||||
McpServer server = McpServerBuilder.builder()
|
|
||||||
.serverInfo("my-server", "1.0.0")
|
|
||||||
.capabilities(cap -> cap
|
|
||||||
.tools(true)
|
|
||||||
.resources(true)
|
|
||||||
.prompts(true))
|
|
||||||
.build();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool Handler
|
|
||||||
```java
|
|
||||||
server.addToolHandler("process", (args) -> {
|
|
||||||
return Mono.fromCallable(() -> {
|
|
||||||
String result = process(args);
|
|
||||||
return ToolResponse.success()
|
|
||||||
.addTextContent(result)
|
|
||||||
.build();
|
|
||||||
}).subscribeOn(Schedulers.boundedElastic());
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Transport Configuration
|
|
||||||
```java
|
|
||||||
StdioServerTransport transport = new StdioServerTransport();
|
|
||||||
server.start(transport).subscribe();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Spring Boot Integration
|
|
||||||
```java
|
|
||||||
@Configuration
|
|
||||||
public class McpConfiguration {
|
|
||||||
@Bean
|
|
||||||
public McpServerConfigurer mcpServerConfigurer() {
|
|
||||||
return server -> server
|
|
||||||
.serverInfo("spring-server", "1.0.0")
|
|
||||||
.capabilities(cap -> cap.tools(true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### Reactive Streams
|
|
||||||
Use Mono for single results, Flux for streams:
|
|
||||||
```java
|
|
||||||
// Single result
|
|
||||||
Mono<ToolResponse> result = Mono.just(
|
|
||||||
ToolResponse.success().build()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Stream of items
|
|
||||||
Flux<Resource> resources = Flux.fromIterable(getResources());
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
Proper error handling in reactive chains:
|
|
||||||
```java
|
|
||||||
server.addToolHandler("risky", (args) -> {
|
|
||||||
return Mono.fromCallable(() -> riskyOperation(args))
|
|
||||||
.map(result -> ToolResponse.success()
|
|
||||||
.addTextContent(result)
|
|
||||||
.build())
|
|
||||||
.onErrorResume(ValidationException.class, e ->
|
|
||||||
Mono.just(ToolResponse.error()
|
|
||||||
.message("Invalid input")
|
|
||||||
.build()))
|
|
||||||
.doOnError(e -> log.error("Error", e));
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logging
|
|
||||||
Use SLF4J for structured logging:
|
|
||||||
```java
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
|
|
||||||
|
|
||||||
log.info("Tool called: {}", toolName);
|
|
||||||
log.debug("Processing with args: {}", args);
|
|
||||||
log.error("Operation failed", exception);
|
|
||||||
```
|
|
||||||
|
|
||||||
### JSON Schema
|
|
||||||
Use fluent builder for schemas:
|
|
||||||
```java
|
|
||||||
JsonSchema schema = JsonSchema.object()
|
|
||||||
.property("name", JsonSchema.string()
|
|
||||||
.description("User's name")
|
|
||||||
.required(true))
|
|
||||||
.property("age", JsonSchema.integer()
|
|
||||||
.minimum(0)
|
|
||||||
.maximum(150))
|
|
||||||
.build();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
### Synchronous Facade
|
|
||||||
For blocking operations:
|
|
||||||
```java
|
|
||||||
McpSyncServer syncServer = server.toSyncServer();
|
|
||||||
|
|
||||||
syncServer.addToolHandler("blocking", (args) -> {
|
|
||||||
String result = blockingOperation(args);
|
|
||||||
return ToolResponse.success()
|
|
||||||
.addTextContent(result)
|
|
||||||
.build();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource Subscription
|
|
||||||
Track subscriptions:
|
|
||||||
```java
|
|
||||||
private final Set<String> subscriptions = ConcurrentHashMap.newKeySet();
|
|
||||||
|
|
||||||
server.addResourceSubscribeHandler((uri) -> {
|
|
||||||
subscriptions.add(uri);
|
|
||||||
log.info("Subscribed to {}", uri);
|
|
||||||
return Mono.empty();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Async Operations
|
|
||||||
Use bounded elastic for blocking calls:
|
|
||||||
```java
|
|
||||||
server.addToolHandler("external", (args) -> {
|
|
||||||
return Mono.fromCallable(() -> callExternalApi(args))
|
|
||||||
.timeout(Duration.ofSeconds(30))
|
|
||||||
.subscribeOn(Schedulers.boundedElastic());
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Context Propagation
|
|
||||||
Propagate observability context:
|
|
||||||
```java
|
|
||||||
server.addToolHandler("traced", (args) -> {
|
|
||||||
return Mono.deferContextual(ctx -> {
|
|
||||||
String traceId = ctx.get("traceId");
|
|
||||||
log.info("Processing with traceId: {}", traceId);
|
|
||||||
return processWithContext(args, traceId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Spring Boot Integration
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
```java
|
|
||||||
@Configuration
|
|
||||||
public class McpConfig {
|
|
||||||
@Bean
|
|
||||||
public McpServerConfigurer configurer() {
|
|
||||||
return server -> server
|
|
||||||
.serverInfo("spring-app", "1.0.0")
|
|
||||||
.capabilities(cap -> cap
|
|
||||||
.tools(true)
|
|
||||||
.resources(true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Component-Based Handlers
|
|
||||||
```java
|
|
||||||
@Component
|
|
||||||
public class SearchToolHandler implements ToolHandler {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "search";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Tool getTool() {
|
|
||||||
return Tool.builder()
|
|
||||||
.name("search")
|
|
||||||
.description("Search for data")
|
|
||||||
.inputSchema(JsonSchema.object()
|
|
||||||
.property("query", JsonSchema.string().required(true)))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Mono<ToolResponse> handle(JsonNode args) {
|
|
||||||
String query = args.get("query").asText();
|
|
||||||
return searchService.search(query)
|
|
||||||
.map(results -> ToolResponse.success()
|
|
||||||
.addTextContent(results)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
```java
|
|
||||||
@Test
|
|
||||||
void testToolHandler() {
|
|
||||||
McpServer server = createTestServer();
|
|
||||||
McpSyncServer syncServer = server.toSyncServer();
|
|
||||||
|
|
||||||
ObjectNode args = new ObjectMapper().createObjectNode()
|
|
||||||
.put("key", "value");
|
|
||||||
|
|
||||||
ToolResponse response = syncServer.callTool("test", args);
|
|
||||||
|
|
||||||
assertFalse(response.isError());
|
|
||||||
assertEquals(1, response.getContent().size());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reactive Tests
|
|
||||||
```java
|
|
||||||
@Test
|
|
||||||
void testReactiveHandler() {
|
|
||||||
Mono<ToolResponse> result = toolHandler.handle(args);
|
|
||||||
|
|
||||||
StepVerifier.create(result)
|
|
||||||
.expectNextMatches(response -> !response.isError())
|
|
||||||
.verifyComplete();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Platform Support
|
|
||||||
|
|
||||||
The Java SDK supports:
|
|
||||||
- Java 17+ (LTS recommended)
|
|
||||||
- Jakarta Servlet 5.0+
|
|
||||||
- Spring Boot 3.0+
|
|
||||||
- Project Reactor 3.5+
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Modules
|
|
||||||
- `mcp-core` - Core implementation (stdio, JDK HttpClient, Servlet)
|
|
||||||
- `mcp-json` - JSON abstraction layer
|
|
||||||
- `mcp-jackson2` - Jackson implementation
|
|
||||||
- `mcp` - Convenience bundle (core + Jackson)
|
|
||||||
- `mcp-spring` - Spring integrations (WebClient, WebFlux, WebMVC)
|
|
||||||
|
|
||||||
### Design Decisions
|
|
||||||
- **JSON**: Jackson behind abstraction (`mcp-json`)
|
|
||||||
- **Async**: Reactive Streams with Project Reactor
|
|
||||||
- **HTTP Client**: JDK HttpClient (Java 11+)
|
|
||||||
- **HTTP Server**: Jakarta Servlet, Spring WebFlux/WebMVC
|
|
||||||
- **Logging**: SLF4J facade
|
|
||||||
- **Observability**: Reactor Context
|
|
||||||
|
|
||||||
## Ask Me About
|
|
||||||
|
|
||||||
- Server setup and configuration
|
|
||||||
- Tool, resource, and prompt implementations
|
|
||||||
- Reactive Streams patterns with Reactor
|
|
||||||
- Spring Boot integration and starters
|
|
||||||
- JSON schema construction
|
|
||||||
- Error handling strategies
|
|
||||||
- Testing reactive code
|
|
||||||
- HTTP transport configuration
|
|
||||||
- Servlet integration
|
|
||||||
- Context propagation for tracing
|
|
||||||
- Performance optimization
|
|
||||||
- Deployment strategies
|
|
||||||
- Maven and Gradle setup
|
|
||||||
|
|
||||||
I'm here to help you build efficient, scalable, and idiomatic Java MCP servers. What would you like to work on?
|
|
||||||
@ -1,756 +0,0 @@
|
|||||||
---
|
|
||||||
description: 'Generate a complete Model Context Protocol server project in Java using the official MCP Java SDK with reactive streams and optional Spring Boot integration.'
|
|
||||||
mode: agent
|
|
||||||
---
|
|
||||||
|
|
||||||
# Java MCP Server Generator
|
|
||||||
|
|
||||||
Generate a complete, production-ready MCP server in Java using the official Java SDK with Maven or Gradle.
|
|
||||||
|
|
||||||
## Project Generation
|
|
||||||
|
|
||||||
When asked to create a Java MCP server, generate a complete project with this structure:
|
|
||||||
|
|
||||||
```
|
|
||||||
my-mcp-server/
|
|
||||||
├── pom.xml (or build.gradle.kts)
|
|
||||||
├── src/
|
|
||||||
│ ├── main/
|
|
||||||
│ │ ├── java/
|
|
||||||
│ │ │ └── com/example/mcp/
|
|
||||||
│ │ │ ├── McpServerApplication.java
|
|
||||||
│ │ │ ├── config/
|
|
||||||
│ │ │ │ └── ServerConfiguration.java
|
|
||||||
│ │ │ ├── tools/
|
|
||||||
│ │ │ │ ├── ToolDefinitions.java
|
|
||||||
│ │ │ │ └── ToolHandlers.java
|
|
||||||
│ │ │ ├── resources/
|
|
||||||
│ │ │ │ ├── ResourceDefinitions.java
|
|
||||||
│ │ │ │ └── ResourceHandlers.java
|
|
||||||
│ │ │ └── prompts/
|
|
||||||
│ │ │ ├── PromptDefinitions.java
|
|
||||||
│ │ │ └── PromptHandlers.java
|
|
||||||
│ │ └── resources/
|
|
||||||
│ │ └── application.properties (if using Spring)
|
|
||||||
│ └── test/
|
|
||||||
│ └── java/
|
|
||||||
│ └── com/example/mcp/
|
|
||||||
│ └── McpServerTest.java
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Maven pom.xml Template
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
|
||||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<groupId>com.example</groupId>
|
|
||||||
<artifactId>my-mcp-server</artifactId>
|
|
||||||
<version>1.0.0</version>
|
|
||||||
<packaging>jar</packaging>
|
|
||||||
|
|
||||||
<name>My MCP Server</name>
|
|
||||||
<description>Model Context Protocol server implementation</description>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<java.version>17</java.version>
|
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
||||||
<mcp.version>0.14.1</mcp.version>
|
|
||||||
<slf4j.version>2.0.9</slf4j.version>
|
|
||||||
<logback.version>1.4.11</logback.version>
|
|
||||||
<junit.version>5.10.0</junit.version>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<!-- MCP Java SDK -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
|
||||||
<artifactId>mcp</artifactId>
|
|
||||||
<version>${mcp.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Logging -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.slf4j</groupId>
|
|
||||||
<artifactId>slf4j-api</artifactId>
|
|
||||||
<version>${slf4j.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>ch.qos.logback</groupId>
|
|
||||||
<artifactId>logback-classic</artifactId>
|
|
||||||
<version>${logback.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Testing -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.junit.jupiter</groupId>
|
|
||||||
<artifactId>junit-jupiter</artifactId>
|
|
||||||
<version>${junit.version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.projectreactor</groupId>
|
|
||||||
<artifactId>reactor-test</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
<version>3.11.0</version>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
|
||||||
<version>3.1.2</version>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
|
||||||
<version>3.5.0</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>shade</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<transformers>
|
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
|
||||||
<mainClass>com.example.mcp.McpServerApplication</mainClass>
|
|
||||||
</transformer>
|
|
||||||
</transformers>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</project>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Gradle build.gradle.kts Template
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
plugins {
|
|
||||||
id("java")
|
|
||||||
id("application")
|
|
||||||
}
|
|
||||||
|
|
||||||
group = "com.example"
|
|
||||||
version = "1.0.0"
|
|
||||||
|
|
||||||
java {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// MCP Java SDK
|
|
||||||
implementation("io.modelcontextprotocol.sdk:mcp:0.14.1")
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
implementation("org.slf4j:slf4j-api:2.0.9")
|
|
||||||
implementation("ch.qos.logback:logback-classic:1.4.11")
|
|
||||||
|
|
||||||
// Testing
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
|
|
||||||
testImplementation("io.projectreactor:reactor-test:3.5.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
application {
|
|
||||||
mainClass.set("com.example.mcp.McpServerApplication")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## McpServerApplication.java Template
|
|
||||||
|
|
||||||
```java
|
|
||||||
package com.example.mcp;
|
|
||||||
|
|
||||||
import com.example.mcp.tools.ToolHandlers;
|
|
||||||
import com.example.mcp.resources.ResourceHandlers;
|
|
||||||
import com.example.mcp.prompts.PromptHandlers;
|
|
||||||
import io.mcp.server.McpServer;
|
|
||||||
import io.mcp.server.McpServerBuilder;
|
|
||||||
import io.mcp.server.transport.StdioServerTransport;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import reactor.core.Disposable;
|
|
||||||
|
|
||||||
public class McpServerApplication {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(McpServerApplication.class);
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
log.info("Starting MCP Server...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
McpServer server = createServer();
|
|
||||||
StdioServerTransport transport = new StdioServerTransport();
|
|
||||||
|
|
||||||
// Start server
|
|
||||||
Disposable serverDisposable = server.start(transport).subscribe();
|
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
|
||||||
log.info("Shutting down MCP server");
|
|
||||||
serverDisposable.dispose();
|
|
||||||
server.stop().block();
|
|
||||||
}));
|
|
||||||
|
|
||||||
log.info("MCP Server started successfully");
|
|
||||||
|
|
||||||
// Keep running
|
|
||||||
Thread.currentThread().join();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to start MCP server", e);
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static McpServer createServer() {
|
|
||||||
McpServer server = McpServerBuilder.builder()
|
|
||||||
.serverInfo("my-mcp-server", "1.0.0")
|
|
||||||
.capabilities(capabilities -> capabilities
|
|
||||||
.tools(true)
|
|
||||||
.resources(true)
|
|
||||||
.prompts(true))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Register handlers
|
|
||||||
ToolHandlers.register(server);
|
|
||||||
ResourceHandlers.register(server);
|
|
||||||
PromptHandlers.register(server);
|
|
||||||
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ToolDefinitions.java Template
|
|
||||||
|
|
||||||
```java
|
|
||||||
package com.example.mcp.tools;
|
|
||||||
|
|
||||||
import io.mcp.json.JsonSchema;
|
|
||||||
import io.mcp.server.tool.Tool;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ToolDefinitions {
|
|
||||||
|
|
||||||
public static List<Tool> getTools() {
|
|
||||||
return List.of(
|
|
||||||
createGreetTool(),
|
|
||||||
createCalculateTool()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Tool createGreetTool() {
|
|
||||||
return Tool.builder()
|
|
||||||
.name("greet")
|
|
||||||
.description("Generate a greeting message")
|
|
||||||
.inputSchema(JsonSchema.object()
|
|
||||||
.property("name", JsonSchema.string()
|
|
||||||
.description("Name to greet")
|
|
||||||
.required(true)))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Tool createCalculateTool() {
|
|
||||||
return Tool.builder()
|
|
||||||
.name("calculate")
|
|
||||||
.description("Perform mathematical calculations")
|
|
||||||
.inputSchema(JsonSchema.object()
|
|
||||||
.property("operation", JsonSchema.string()
|
|
||||||
.description("Operation to perform")
|
|
||||||
.enumValues(List.of("add", "subtract", "multiply", "divide"))
|
|
||||||
.required(true))
|
|
||||||
.property("a", JsonSchema.number()
|
|
||||||
.description("First operand")
|
|
||||||
.required(true))
|
|
||||||
.property("b", JsonSchema.number()
|
|
||||||
.description("Second operand")
|
|
||||||
.required(true)))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ToolHandlers.java Template
|
|
||||||
|
|
||||||
```java
|
|
||||||
package com.example.mcp.tools;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import io.mcp.server.McpServer;
|
|
||||||
import io.mcp.server.tool.ToolResponse;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
public class ToolHandlers {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ToolHandlers.class);
|
|
||||||
|
|
||||||
public static void register(McpServer server) {
|
|
||||||
// Register tool list handler
|
|
||||||
server.addToolListHandler(() -> {
|
|
||||||
log.debug("Listing available tools");
|
|
||||||
return Mono.just(ToolDefinitions.getTools());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register greet handler
|
|
||||||
server.addToolHandler("greet", ToolHandlers::handleGreet);
|
|
||||||
|
|
||||||
// Register calculate handler
|
|
||||||
server.addToolHandler("calculate", ToolHandlers::handleCalculate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Mono<ToolResponse> handleGreet(JsonNode arguments) {
|
|
||||||
log.info("Greet tool called");
|
|
||||||
|
|
||||||
if (!arguments.has("name")) {
|
|
||||||
return Mono.just(ToolResponse.error()
|
|
||||||
.message("Missing 'name' parameter")
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
String name = arguments.get("name").asText();
|
|
||||||
String greeting = "Hello, " + name + "! Welcome to MCP.";
|
|
||||||
|
|
||||||
log.debug("Generated greeting for: {}", name);
|
|
||||||
|
|
||||||
return Mono.just(ToolResponse.success()
|
|
||||||
.addTextContent(greeting)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Mono<ToolResponse> handleCalculate(JsonNode arguments) {
|
|
||||||
log.info("Calculate tool called");
|
|
||||||
|
|
||||||
if (!arguments.has("operation") || !arguments.has("a") || !arguments.has("b")) {
|
|
||||||
return Mono.just(ToolResponse.error()
|
|
||||||
.message("Missing required parameters")
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
String operation = arguments.get("operation").asText();
|
|
||||||
double a = arguments.get("a").asDouble();
|
|
||||||
double b = arguments.get("b").asDouble();
|
|
||||||
|
|
||||||
double result;
|
|
||||||
switch (operation) {
|
|
||||||
case "add":
|
|
||||||
result = a + b;
|
|
||||||
break;
|
|
||||||
case "subtract":
|
|
||||||
result = a - b;
|
|
||||||
break;
|
|
||||||
case "multiply":
|
|
||||||
result = a * b;
|
|
||||||
break;
|
|
||||||
case "divide":
|
|
||||||
if (b == 0) {
|
|
||||||
return Mono.just(ToolResponse.error()
|
|
||||||
.message("Division by zero")
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
result = a / b;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return Mono.just(ToolResponse.error()
|
|
||||||
.message("Unknown operation: " + operation)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("Calculation: {} {} {} = {}", a, operation, b, result);
|
|
||||||
|
|
||||||
return Mono.just(ToolResponse.success()
|
|
||||||
.addTextContent("Result: " + result)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ResourceDefinitions.java Template
|
|
||||||
|
|
||||||
```java
|
|
||||||
package com.example.mcp.resources;
|
|
||||||
|
|
||||||
import io.mcp.server.resource.Resource;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ResourceDefinitions {
|
|
||||||
|
|
||||||
public static List<Resource> getResources() {
|
|
||||||
return List.of(
|
|
||||||
Resource.builder()
|
|
||||||
.name("Example Data")
|
|
||||||
.uri("resource://data/example")
|
|
||||||
.description("Example resource data")
|
|
||||||
.mimeType("application/json")
|
|
||||||
.build(),
|
|
||||||
Resource.builder()
|
|
||||||
.name("Configuration")
|
|
||||||
.uri("resource://config")
|
|
||||||
.description("Server configuration")
|
|
||||||
.mimeType("application/json")
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ResourceHandlers.java Template
|
|
||||||
|
|
||||||
```java
|
|
||||||
package com.example.mcp.resources;
|
|
||||||
|
|
||||||
import io.mcp.server.McpServer;
|
|
||||||
import io.mcp.server.resource.ResourceContent;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
public class ResourceHandlers {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ResourceHandlers.class);
|
|
||||||
private static final Map<String, Boolean> subscriptions = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public static void register(McpServer server) {
|
|
||||||
// Register resource list handler
|
|
||||||
server.addResourceListHandler(() -> {
|
|
||||||
log.debug("Listing available resources");
|
|
||||||
return Mono.just(ResourceDefinitions.getResources());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register resource read handler
|
|
||||||
server.addResourceReadHandler(ResourceHandlers::handleRead);
|
|
||||||
|
|
||||||
// Register resource subscribe handler
|
|
||||||
server.addResourceSubscribeHandler(ResourceHandlers::handleSubscribe);
|
|
||||||
|
|
||||||
// Register resource unsubscribe handler
|
|
||||||
server.addResourceUnsubscribeHandler(ResourceHandlers::handleUnsubscribe);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Mono<ResourceContent> handleRead(String uri) {
|
|
||||||
log.info("Reading resource: {}", uri);
|
|
||||||
|
|
||||||
switch (uri) {
|
|
||||||
case "resource://data/example":
|
|
||||||
String jsonData = String.format(
|
|
||||||
"{\"message\":\"Example resource data\",\"timestamp\":\"%s\"}",
|
|
||||||
Instant.now()
|
|
||||||
);
|
|
||||||
return Mono.just(ResourceContent.text(jsonData, uri, "application/json"));
|
|
||||||
|
|
||||||
case "resource://config":
|
|
||||||
String config = "{\"serverName\":\"my-mcp-server\",\"version\":\"1.0.0\"}";
|
|
||||||
return Mono.just(ResourceContent.text(config, uri, "application/json"));
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.warn("Unknown resource requested: {}", uri);
|
|
||||||
return Mono.error(new IllegalArgumentException("Unknown resource URI: " + uri));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Mono<Void> handleSubscribe(String uri) {
|
|
||||||
log.info("Client subscribed to resource: {}", uri);
|
|
||||||
subscriptions.put(uri, true);
|
|
||||||
return Mono.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Mono<Void> handleUnsubscribe(String uri) {
|
|
||||||
log.info("Client unsubscribed from resource: {}", uri);
|
|
||||||
subscriptions.remove(uri);
|
|
||||||
return Mono.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## PromptDefinitions.java Template
|
|
||||||
|
|
||||||
```java
|
|
||||||
package com.example.mcp.prompts;
|
|
||||||
|
|
||||||
import io.mcp.server.prompt.Prompt;
|
|
||||||
import io.mcp.server.prompt.PromptArgument;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class PromptDefinitions {
|
|
||||||
|
|
||||||
public static List<Prompt> getPrompts() {
|
|
||||||
return List.of(
|
|
||||||
Prompt.builder()
|
|
||||||
.name("code-review")
|
|
||||||
.description("Generate a code review prompt")
|
|
||||||
.argument(PromptArgument.builder()
|
|
||||||
.name("language")
|
|
||||||
.description("Programming language")
|
|
||||||
.required(true)
|
|
||||||
.build())
|
|
||||||
.argument(PromptArgument.builder()
|
|
||||||
.name("focus")
|
|
||||||
.description("Review focus area")
|
|
||||||
.required(false)
|
|
||||||
.build())
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## PromptHandlers.java Template
|
|
||||||
|
|
||||||
```java
|
|
||||||
package com.example.mcp.prompts;
|
|
||||||
|
|
||||||
import io.mcp.server.McpServer;
|
|
||||||
import io.mcp.server.prompt.PromptMessage;
|
|
||||||
import io.mcp.server.prompt.PromptResult;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class PromptHandlers {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(PromptHandlers.class);
|
|
||||||
|
|
||||||
public static void register(McpServer server) {
|
|
||||||
// Register prompt list handler
|
|
||||||
server.addPromptListHandler(() -> {
|
|
||||||
log.debug("Listing available prompts");
|
|
||||||
return Mono.just(PromptDefinitions.getPrompts());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register prompt get handler
|
|
||||||
server.addPromptGetHandler(PromptHandlers::handleCodeReview);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Mono<PromptResult> handleCodeReview(String name, Map<String, String> arguments) {
|
|
||||||
log.info("Getting prompt: {}", name);
|
|
||||||
|
|
||||||
if (!name.equals("code-review")) {
|
|
||||||
return Mono.error(new IllegalArgumentException("Unknown prompt: " + name));
|
|
||||||
}
|
|
||||||
|
|
||||||
String language = arguments.getOrDefault("language", "Java");
|
|
||||||
String focus = arguments.getOrDefault("focus", "general quality");
|
|
||||||
|
|
||||||
String description = "Code review for " + language + " with focus on " + focus;
|
|
||||||
|
|
||||||
List<PromptMessage> messages = List.of(
|
|
||||||
PromptMessage.user("Please review this " + language + " code with focus on " + focus + "."),
|
|
||||||
PromptMessage.assistant("I'll review the code focusing on " + focus + ". Please share the code."),
|
|
||||||
PromptMessage.user("Here's the code to review: [paste code here]")
|
|
||||||
);
|
|
||||||
|
|
||||||
log.debug("Generated code review prompt for {} ({})", language, focus);
|
|
||||||
|
|
||||||
return Mono.just(PromptResult.builder()
|
|
||||||
.description(description)
|
|
||||||
.messages(messages)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## McpServerTest.java Template
|
|
||||||
|
|
||||||
```java
|
|
||||||
package com.example.mcp;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import io.mcp.server.McpServer;
|
|
||||||
import io.mcp.server.McpSyncServer;
|
|
||||||
import io.mcp.server.tool.ToolResponse;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
class McpServerTest {
|
|
||||||
|
|
||||||
private McpSyncServer syncServer;
|
|
||||||
private ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
McpServer server = createTestServer();
|
|
||||||
syncServer = server.toSyncServer();
|
|
||||||
objectMapper = new ObjectMapper();
|
|
||||||
}
|
|
||||||
|
|
||||||
private McpServer createTestServer() {
|
|
||||||
// Same setup as main application
|
|
||||||
McpServer server = McpServerBuilder.builder()
|
|
||||||
.serverInfo("test-server", "1.0.0")
|
|
||||||
.capabilities(cap -> cap.tools(true))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Register handlers
|
|
||||||
ToolHandlers.register(server);
|
|
||||||
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGreetTool() {
|
|
||||||
ObjectNode args = objectMapper.createObjectNode();
|
|
||||||
args.put("name", "Java");
|
|
||||||
|
|
||||||
ToolResponse response = syncServer.callTool("greet", args);
|
|
||||||
|
|
||||||
assertFalse(response.isError());
|
|
||||||
assertEquals(1, response.getContent().size());
|
|
||||||
assertTrue(response.getContent().get(0).getText().contains("Java"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testCalculateTool() {
|
|
||||||
ObjectNode args = objectMapper.createObjectNode();
|
|
||||||
args.put("operation", "add");
|
|
||||||
args.put("a", 5);
|
|
||||||
args.put("b", 3);
|
|
||||||
|
|
||||||
ToolResponse response = syncServer.callTool("calculate", args);
|
|
||||||
|
|
||||||
assertFalse(response.isError());
|
|
||||||
assertTrue(response.getContent().get(0).getText().contains("8"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testDivideByZero() {
|
|
||||||
ObjectNode args = objectMapper.createObjectNode();
|
|
||||||
args.put("operation", "divide");
|
|
||||||
args.put("a", 10);
|
|
||||||
args.put("b", 0);
|
|
||||||
|
|
||||||
ToolResponse response = syncServer.callTool("calculate", args);
|
|
||||||
|
|
||||||
assertTrue(response.isError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## README.md Template
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# My MCP Server
|
|
||||||
|
|
||||||
A Model Context Protocol server built with Java and the official MCP Java SDK.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- ✅ Tools: greet, calculate
|
|
||||||
- ✅ Resources: example data, configuration
|
|
||||||
- ✅ Prompts: code-review
|
|
||||||
- ✅ Reactive Streams with Project Reactor
|
|
||||||
- ✅ Structured logging with SLF4J
|
|
||||||
- ✅ Full test coverage
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Java 17 or later
|
|
||||||
- Maven 3.6+ or Gradle 7+
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
### Maven
|
|
||||||
```bash
|
|
||||||
mvn clean package
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gradle
|
|
||||||
```bash
|
|
||||||
./gradlew build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run
|
|
||||||
|
|
||||||
### Maven
|
|
||||||
```bash
|
|
||||||
java -jar target/my-mcp-server-1.0.0.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gradle
|
|
||||||
```bash
|
|
||||||
./gradlew run
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Maven
|
|
||||||
```bash
|
|
||||||
mvn test
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gradle
|
|
||||||
```bash
|
|
||||||
./gradlew test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration with Claude Desktop
|
|
||||||
|
|
||||||
Add to `claude_desktop_config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"my-mcp-server": {
|
|
||||||
"command": "java",
|
|
||||||
"args": ["-jar", "/path/to/my-mcp-server-1.0.0.jar"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
```
|
|
||||||
|
|
||||||
## Generation Instructions
|
|
||||||
|
|
||||||
1. **Ask for project name and package**
|
|
||||||
2. **Choose build tool** (Maven or Gradle)
|
|
||||||
3. **Generate all files** with proper package structure
|
|
||||||
4. **Use Reactive Streams** for async handlers
|
|
||||||
5. **Include comprehensive logging** with SLF4J
|
|
||||||
6. **Add tests** for all handlers
|
|
||||||
7. **Follow Java conventions** (camelCase, PascalCase)
|
|
||||||
8. **Include error handling** with proper responses
|
|
||||||
9. **Document public APIs** with Javadoc
|
|
||||||
10. **Provide both sync and async** examples
|
|
||||||
@ -1,553 +0,0 @@
|
|||||||
---
|
|
||||||
description: 'Best practices and patterns for building Model Context Protocol (MCP) servers in Java using the official MCP Java SDK with reactive streams and Spring integration.'
|
|
||||||
applyTo: "**/*.java, **/pom.xml, **/build.gradle, **/build.gradle.kts"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Java MCP Server Development Guidelines
|
|
||||||
|
|
||||||
When building MCP servers in Java, follow these best practices and patterns using the official Java SDK.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
Add the MCP Java SDK to your Maven project:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
|
||||||
<artifactId>mcp</artifactId>
|
|
||||||
<version>0.14.1</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
```
|
|
||||||
|
|
||||||
Or for Gradle:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
dependencies {
|
|
||||||
implementation("io.modelcontextprotocol.sdk:mcp:0.14.1")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server Setup
|
|
||||||
|
|
||||||
Create an MCP server using the builder pattern:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import io.mcp.server.McpServer;
|
|
||||||
import io.mcp.server.McpServerBuilder;
|
|
||||||
import io.mcp.server.transport.StdioServerTransport;
|
|
||||||
|
|
||||||
McpServer server = McpServerBuilder.builder()
|
|
||||||
.serverInfo("my-server", "1.0.0")
|
|
||||||
.capabilities(capabilities -> capabilities
|
|
||||||
.tools(true)
|
|
||||||
.resources(true)
|
|
||||||
.prompts(true))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Start with stdio transport
|
|
||||||
StdioServerTransport transport = new StdioServerTransport();
|
|
||||||
server.start(transport).subscribe();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding Tools
|
|
||||||
|
|
||||||
Register tool handlers with the server:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import io.mcp.server.tool.Tool;
|
|
||||||
import io.mcp.server.tool.ToolHandler;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
// Define a tool
|
|
||||||
Tool searchTool = Tool.builder()
|
|
||||||
.name("search")
|
|
||||||
.description("Search for information")
|
|
||||||
.inputSchema(JsonSchema.object()
|
|
||||||
.property("query", JsonSchema.string()
|
|
||||||
.description("Search query")
|
|
||||||
.required(true))
|
|
||||||
.property("limit", JsonSchema.integer()
|
|
||||||
.description("Maximum results")
|
|
||||||
.defaultValue(10)))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Register tool handler
|
|
||||||
server.addToolHandler("search", (arguments) -> {
|
|
||||||
String query = arguments.get("query").asText();
|
|
||||||
int limit = arguments.has("limit")
|
|
||||||
? arguments.get("limit").asInt()
|
|
||||||
: 10;
|
|
||||||
|
|
||||||
// Perform search
|
|
||||||
List<String> results = performSearch(query, limit);
|
|
||||||
|
|
||||||
return Mono.just(ToolResponse.success()
|
|
||||||
.addTextContent("Found " + results.size() + " results")
|
|
||||||
.build());
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding Resources
|
|
||||||
|
|
||||||
Implement resource handlers for data access:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import io.mcp.server.resource.Resource;
|
|
||||||
import io.mcp.server.resource.ResourceHandler;
|
|
||||||
|
|
||||||
// Register resource list handler
|
|
||||||
server.addResourceListHandler(() -> {
|
|
||||||
List<Resource> resources = List.of(
|
|
||||||
Resource.builder()
|
|
||||||
.name("Data File")
|
|
||||||
.uri("resource://data/example.txt")
|
|
||||||
.description("Example data file")
|
|
||||||
.mimeType("text/plain")
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
return Mono.just(resources);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register resource read handler
|
|
||||||
server.addResourceReadHandler((uri) -> {
|
|
||||||
if (uri.equals("resource://data/example.txt")) {
|
|
||||||
String content = loadResourceContent(uri);
|
|
||||||
return Mono.just(ResourceContent.text(content, uri));
|
|
||||||
}
|
|
||||||
throw new ResourceNotFoundException(uri);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register resource subscribe handler
|
|
||||||
server.addResourceSubscribeHandler((uri) -> {
|
|
||||||
subscriptions.add(uri);
|
|
||||||
log.info("Client subscribed to {}", uri);
|
|
||||||
return Mono.empty();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding Prompts
|
|
||||||
|
|
||||||
Implement prompt handlers for templated conversations:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import io.mcp.server.prompt.Prompt;
|
|
||||||
import io.mcp.server.prompt.PromptMessage;
|
|
||||||
import io.mcp.server.prompt.PromptArgument;
|
|
||||||
|
|
||||||
// Register prompt list handler
|
|
||||||
server.addPromptListHandler(() -> {
|
|
||||||
List<Prompt> prompts = List.of(
|
|
||||||
Prompt.builder()
|
|
||||||
.name("analyze")
|
|
||||||
.description("Analyze a topic")
|
|
||||||
.argument(PromptArgument.builder()
|
|
||||||
.name("topic")
|
|
||||||
.description("Topic to analyze")
|
|
||||||
.required(true)
|
|
||||||
.build())
|
|
||||||
.argument(PromptArgument.builder()
|
|
||||||
.name("depth")
|
|
||||||
.description("Analysis depth")
|
|
||||||
.required(false)
|
|
||||||
.build())
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
return Mono.just(prompts);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register prompt get handler
|
|
||||||
server.addPromptGetHandler((name, arguments) -> {
|
|
||||||
if (name.equals("analyze")) {
|
|
||||||
String topic = arguments.getOrDefault("topic", "general");
|
|
||||||
String depth = arguments.getOrDefault("depth", "basic");
|
|
||||||
|
|
||||||
List<PromptMessage> messages = List.of(
|
|
||||||
PromptMessage.user("Please analyze this topic: " + topic),
|
|
||||||
PromptMessage.assistant("I'll provide a " + depth + " analysis of " + topic)
|
|
||||||
);
|
|
||||||
|
|
||||||
return Mono.just(PromptResult.builder()
|
|
||||||
.description("Analysis of " + topic + " at " + depth + " level")
|
|
||||||
.messages(messages)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
throw new PromptNotFoundException(name);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Reactive Streams Pattern
|
|
||||||
|
|
||||||
The Java SDK uses Reactive Streams (Project Reactor) for asynchronous processing:
|
|
||||||
|
|
||||||
```java
|
|
||||||
// Return Mono for single results
|
|
||||||
server.addToolHandler("process", (args) -> {
|
|
||||||
return Mono.fromCallable(() -> {
|
|
||||||
String result = expensiveOperation(args);
|
|
||||||
return ToolResponse.success()
|
|
||||||
.addTextContent(result)
|
|
||||||
.build();
|
|
||||||
}).subscribeOn(Schedulers.boundedElastic());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return Flux for streaming results
|
|
||||||
server.addResourceListHandler(() -> {
|
|
||||||
return Flux.fromIterable(getResources())
|
|
||||||
.map(r -> Resource.builder()
|
|
||||||
.uri(r.getUri())
|
|
||||||
.name(r.getName())
|
|
||||||
.build())
|
|
||||||
.collectList();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Synchronous Facade
|
|
||||||
|
|
||||||
For blocking use cases, use the synchronous API:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import io.mcp.server.McpSyncServer;
|
|
||||||
|
|
||||||
McpSyncServer syncServer = server.toSyncServer();
|
|
||||||
|
|
||||||
// Blocking tool handler
|
|
||||||
syncServer.addToolHandler("greet", (args) -> {
|
|
||||||
String name = args.get("name").asText();
|
|
||||||
return ToolResponse.success()
|
|
||||||
.addTextContent("Hello, " + name + "!")
|
|
||||||
.build();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Transport Configuration
|
|
||||||
|
|
||||||
### Stdio Transport
|
|
||||||
|
|
||||||
For local subprocess communication:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import io.mcp.server.transport.StdioServerTransport;
|
|
||||||
|
|
||||||
StdioServerTransport transport = new StdioServerTransport();
|
|
||||||
server.start(transport).block();
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTTP Transport (Servlet)
|
|
||||||
|
|
||||||
For HTTP-based servers:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import io.mcp.server.transport.ServletServerTransport;
|
|
||||||
import jakarta.servlet.http.HttpServlet;
|
|
||||||
|
|
||||||
public class McpServlet extends HttpServlet {
|
|
||||||
private final McpServer server;
|
|
||||||
private final ServletServerTransport transport;
|
|
||||||
|
|
||||||
public McpServlet() {
|
|
||||||
this.server = createMcpServer();
|
|
||||||
this.transport = new ServletServerTransport();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
|
|
||||||
transport.handleRequest(server, req, resp).block();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Spring Boot Integration
|
|
||||||
|
|
||||||
Use the Spring Boot starter for seamless integration:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
|
||||||
<artifactId>mcp-spring-boot-starter</artifactId>
|
|
||||||
<version>0.14.1</version>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
Configure the server with Spring:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import io.mcp.spring.McpServerConfigurer;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class McpConfiguration {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public McpServerConfigurer mcpServerConfigurer() {
|
|
||||||
return server -> server
|
|
||||||
.serverInfo("spring-server", "1.0.0")
|
|
||||||
.capabilities(cap -> cap
|
|
||||||
.tools(true)
|
|
||||||
.resources(true)
|
|
||||||
.prompts(true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Register handlers as Spring beans:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import io.mcp.spring.ToolHandler;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class SearchToolHandler implements ToolHandler {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "search";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Tool getTool() {
|
|
||||||
return Tool.builder()
|
|
||||||
.name("search")
|
|
||||||
.description("Search for information")
|
|
||||||
.inputSchema(JsonSchema.object()
|
|
||||||
.property("query", JsonSchema.string().required(true)))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Mono<ToolResponse> handle(JsonNode arguments) {
|
|
||||||
String query = arguments.get("query").asText();
|
|
||||||
return Mono.just(ToolResponse.success()
|
|
||||||
.addTextContent("Search results for: " + query)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
Use proper error handling with MCP exceptions:
|
|
||||||
|
|
||||||
```java
|
|
||||||
server.addToolHandler("risky", (args) -> {
|
|
||||||
return Mono.fromCallable(() -> {
|
|
||||||
try {
|
|
||||||
String result = riskyOperation(args);
|
|
||||||
return ToolResponse.success()
|
|
||||||
.addTextContent(result)
|
|
||||||
.build();
|
|
||||||
} catch (ValidationException e) {
|
|
||||||
return ToolResponse.error()
|
|
||||||
.message("Invalid input: " + e.getMessage())
|
|
||||||
.build();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Unexpected error", e);
|
|
||||||
return ToolResponse.error()
|
|
||||||
.message("Internal error occurred")
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## JSON Schema Construction
|
|
||||||
|
|
||||||
Use the fluent schema builder:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import io.mcp.json.JsonSchema;
|
|
||||||
|
|
||||||
JsonSchema schema = JsonSchema.object()
|
|
||||||
.property("name", JsonSchema.string()
|
|
||||||
.description("User's name")
|
|
||||||
.minLength(1)
|
|
||||||
.maxLength(100)
|
|
||||||
.required(true))
|
|
||||||
.property("age", JsonSchema.integer()
|
|
||||||
.description("User's age")
|
|
||||||
.minimum(0)
|
|
||||||
.maximum(150))
|
|
||||||
.property("email", JsonSchema.string()
|
|
||||||
.description("Email address")
|
|
||||||
.format("email")
|
|
||||||
.required(true))
|
|
||||||
.property("tags", JsonSchema.array()
|
|
||||||
.items(JsonSchema.string())
|
|
||||||
.uniqueItems(true))
|
|
||||||
.additionalProperties(false)
|
|
||||||
.build();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logging and Observability
|
|
||||||
|
|
||||||
Use SLF4J for logging:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(MyMcpServer.class);
|
|
||||||
|
|
||||||
server.addToolHandler("process", (args) -> {
|
|
||||||
log.info("Tool called: process, args: {}", args);
|
|
||||||
|
|
||||||
return Mono.fromCallable(() -> {
|
|
||||||
String result = process(args);
|
|
||||||
log.debug("Processing completed successfully");
|
|
||||||
return ToolResponse.success()
|
|
||||||
.addTextContent(result)
|
|
||||||
.build();
|
|
||||||
}).doOnError(error -> {
|
|
||||||
log.error("Processing failed", error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Propagate context with Reactor:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import reactor.util.context.Context;
|
|
||||||
|
|
||||||
server.addToolHandler("traced", (args) -> {
|
|
||||||
return Mono.deferContextual(ctx -> {
|
|
||||||
String traceId = ctx.get("traceId");
|
|
||||||
log.info("Processing with traceId: {}", traceId);
|
|
||||||
|
|
||||||
return Mono.just(ToolResponse.success()
|
|
||||||
.addTextContent("Processed")
|
|
||||||
.build());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Write tests using the synchronous API:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import static org.assertj.core.Assertions.assertThat;
|
|
||||||
|
|
||||||
class McpServerTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testToolHandler() {
|
|
||||||
McpServer server = createTestServer();
|
|
||||||
McpSyncServer syncServer = server.toSyncServer();
|
|
||||||
|
|
||||||
JsonNode args = objectMapper.createObjectNode()
|
|
||||||
.put("query", "test");
|
|
||||||
|
|
||||||
ToolResponse response = syncServer.callTool("search", args);
|
|
||||||
|
|
||||||
assertThat(response.isError()).isFalse();
|
|
||||||
assertThat(response.getContent()).hasSize(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Jackson Integration
|
|
||||||
|
|
||||||
The SDK uses Jackson for JSON serialization. Customize as needed:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
|
||||||
|
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
|
||||||
mapper.registerModule(new JavaTimeModule());
|
|
||||||
|
|
||||||
// Use custom mapper with server
|
|
||||||
McpServer server = McpServerBuilder.builder()
|
|
||||||
.objectMapper(mapper)
|
|
||||||
.build();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Content Types
|
|
||||||
|
|
||||||
Support multiple content types in responses:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import io.mcp.server.content.Content;
|
|
||||||
|
|
||||||
server.addToolHandler("multi", (args) -> {
|
|
||||||
return Mono.just(ToolResponse.success()
|
|
||||||
.addTextContent("Plain text response")
|
|
||||||
.addImageContent(imageBytes, "image/png")
|
|
||||||
.addResourceContent("resource://data", "application/json", jsonData)
|
|
||||||
.build());
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server Lifecycle
|
|
||||||
|
|
||||||
Properly manage server lifecycle:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import reactor.core.Disposable;
|
|
||||||
|
|
||||||
Disposable serverDisposable = server.start(transport).subscribe();
|
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
|
||||||
log.info("Shutting down MCP server");
|
|
||||||
serverDisposable.dispose();
|
|
||||||
server.stop().block();
|
|
||||||
}));
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
### Request Validation
|
|
||||||
|
|
||||||
```java
|
|
||||||
server.addToolHandler("validate", (args) -> {
|
|
||||||
if (!args.has("required_field")) {
|
|
||||||
return Mono.just(ToolResponse.error()
|
|
||||||
.message("Missing required_field")
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
return processRequest(args);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Async Operations
|
|
||||||
|
|
||||||
```java
|
|
||||||
server.addToolHandler("async", (args) -> {
|
|
||||||
return Mono.fromCallable(() -> callExternalApi(args))
|
|
||||||
.timeout(Duration.ofSeconds(30))
|
|
||||||
.onErrorResume(TimeoutException.class, e ->
|
|
||||||
Mono.just(ToolResponse.error()
|
|
||||||
.message("Operation timed out")
|
|
||||||
.build()))
|
|
||||||
.subscribeOn(Schedulers.boundedElastic());
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource Caching
|
|
||||||
|
|
||||||
```java
|
|
||||||
private final Map<String, String> cache = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
server.addResourceReadHandler((uri) -> {
|
|
||||||
return Mono.fromCallable(() ->
|
|
||||||
cache.computeIfAbsent(uri, this::loadResource))
|
|
||||||
.map(content -> ResourceContent.text(content, uri));
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Use Reactive Streams** for async operations and backpressure
|
|
||||||
2. **Leverage Spring Boot** starter for enterprise applications
|
|
||||||
3. **Implement proper error handling** with specific error messages
|
|
||||||
4. **Use SLF4J** for logging, not System.out
|
|
||||||
5. **Validate inputs** in tool and prompt handlers
|
|
||||||
6. **Support graceful shutdown** with proper resource cleanup
|
|
||||||
7. **Use bounded elastic scheduler** for blocking operations
|
|
||||||
8. **Propagate context** for observability in reactive chains
|
|
||||||
9. **Test with synchronous API** for simplicity
|
|
||||||
10. **Follow Java naming conventions** (camelCase for methods, PascalCase for classes)
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
id: ruby-mcp-development
|
|
||||||
name: Ruby MCP Server Development
|
|
||||||
description: 'Complete toolkit for building Model Context Protocol servers in Ruby using the official MCP Ruby SDK gem with Rails integration support.'
|
|
||||||
tags: [ruby, mcp, model-context-protocol, server-development, sdk, rails, gem]
|
|
||||||
items:
|
|
||||||
- path: instructions/ruby-mcp-server.instructions.md
|
|
||||||
kind: instruction
|
|
||||||
- path: prompts/ruby-mcp-server-generator.prompt.md
|
|
||||||
kind: prompt
|
|
||||||
- path: chatmodes/ruby-mcp-expert.chatmode.md
|
|
||||||
kind: chat-mode
|
|
||||||
usage: |
|
|
||||||
recommended
|
|
||||||
|
|
||||||
This chat mode provides expert guidance for building MCP servers in Ruby.
|
|
||||||
|
|
||||||
This chat mode is ideal for:
|
|
||||||
- Creating new MCP server projects with Ruby
|
|
||||||
- Implementing tools, prompts, and resources
|
|
||||||
- Setting up stdio or HTTP transports
|
|
||||||
- Debugging schema definitions and error handling
|
|
||||||
- Learning Ruby MCP best practices with the official SDK
|
|
||||||
- Integrating with Rails applications
|
|
||||||
|
|
||||||
To get the best results, consider:
|
|
||||||
- Using the instruction file to set context for Ruby MCP development
|
|
||||||
- Using the prompt to generate initial project structure
|
|
||||||
- Switching to the expert chat mode for detailed implementation help
|
|
||||||
- Specifying whether you need stdio or Rails integration
|
|
||||||
- Providing details about what tools or functionality you need
|
|
||||||
- Mentioning if you need authentication or server_context usage
|
|
||||||
|
|
||||||
display:
|
|
||||||
ordering: manual
|
|
||||||
show_badge: true
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
# Ruby MCP Server Development
|
|
||||||
|
|
||||||
'Complete toolkit for building Model Context Protocol servers in Ruby using the official MCP Ruby SDK gem with Rails integration support.'
|
|
||||||
|
|
||||||
**Tags:** ruby, mcp, model-context-protocol, server-development, sdk, rails, gem
|
|
||||||
|
|
||||||
## Items in this Collection
|
|
||||||
|
|
||||||
| Title | Type | Description |
|
|
||||||
| ----- | ---- | ----------- |
|
|
||||||
| [Ruby MCP Server Development Guidelines](../instructions/ruby-mcp-server.instructions.md)<br />[](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fruby-mcp-server.instructions.md)<br />[](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fruby-mcp-server.instructions.md) | Instruction | Best practices and patterns for building Model Context Protocol (MCP) servers in Ruby using the official MCP Ruby SDK gem. |
|
|
||||||
| [Ruby MCP Server Generator](../prompts/ruby-mcp-server-generator.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fruby-mcp-server-generator.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode-insiders%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fruby-mcp-server-generator.prompt.md) | Prompt | Generate a complete Model Context Protocol server project in Ruby using the official MCP Ruby SDK gem. |
|
|
||||||
| [Ruby MCP Expert](../chatmodes/ruby-mcp-expert.chatmode.md)<br />[](https://aka.ms/awesome-copilot/install/chatmode?url=vscode%3Achat-mode%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fchatmodes%2Fruby-mcp-expert.chatmode.md)<br />[](https://aka.ms/awesome-copilot/install/chatmode?url=vscode-insiders%3Achat-mode%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fchatmodes%2Fruby-mcp-expert.chatmode.md) | Chat Mode | Expert assistance for building Model Context Protocol servers in Ruby using the official MCP Ruby SDK gem with Rails integration. [see usage](#ruby-mcp-expert) |
|
|
||||||
|
|
||||||
## Collection Usage
|
|
||||||
|
|
||||||
### Ruby MCP Expert
|
|
||||||
|
|
||||||
recommended
|
|
||||||
|
|
||||||
This chat mode provides expert guidance for building MCP servers in Ruby.
|
|
||||||
|
|
||||||
This chat mode is ideal for:
|
|
||||||
- Creating new MCP server projects with Ruby
|
|
||||||
- Implementing tools, prompts, and resources
|
|
||||||
- Setting up stdio or HTTP transports
|
|
||||||
- Debugging schema definitions and error handling
|
|
||||||
- Learning Ruby MCP best practices with the official SDK
|
|
||||||
- Integrating with Rails applications
|
|
||||||
|
|
||||||
To get the best results, consider:
|
|
||||||
- Using the instruction file to set context for Ruby MCP development
|
|
||||||
- Using the prompt to generate initial project structure
|
|
||||||
- Switching to the expert chat mode for detailed implementation help
|
|
||||||
- Specifying whether you need stdio or Rails integration
|
|
||||||
- Providing details about what tools or functionality you need
|
|
||||||
- Mentioning if you need authentication or server_context usage
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*This collection includes 3 curated items for ruby mcp server development.*
|
|
||||||
@ -1,346 +0,0 @@
|
|||||||
---
|
|
||||||
description: 'Expert assistance for building Model Context Protocol servers in Ruby using the official MCP Ruby SDK gem with Rails integration.'
|
|
||||||
model: GPT-4.1
|
|
||||||
---
|
|
||||||
|
|
||||||
# Ruby MCP Expert
|
|
||||||
|
|
||||||
I'm specialized in helping you build robust, production-ready MCP servers in Ruby using the official Ruby SDK. I can assist with:
|
|
||||||
|
|
||||||
## Core Capabilities
|
|
||||||
|
|
||||||
### Server Architecture
|
|
||||||
- Setting up MCP::Server instances
|
|
||||||
- Configuring tools, prompts, and resources
|
|
||||||
- Implementing stdio and HTTP transports
|
|
||||||
- Rails controller integration
|
|
||||||
- Server context for authentication
|
|
||||||
|
|
||||||
### Tool Development
|
|
||||||
- Creating tool classes with MCP::Tool
|
|
||||||
- Defining input/output schemas
|
|
||||||
- Implementing tool annotations
|
|
||||||
- Structured content in responses
|
|
||||||
- Error handling with is_error flag
|
|
||||||
|
|
||||||
### Resource Management
|
|
||||||
- Defining resources and resource templates
|
|
||||||
- Implementing resource read handlers
|
|
||||||
- URI template patterns
|
|
||||||
- Dynamic resource generation
|
|
||||||
|
|
||||||
### Prompt Engineering
|
|
||||||
- Creating prompt classes with MCP::Prompt
|
|
||||||
- Defining prompt arguments
|
|
||||||
- Multi-turn conversation templates
|
|
||||||
- Dynamic prompt generation with server_context
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
- Exception reporting with Bugsnag/Sentry
|
|
||||||
- Instrumentation callbacks for metrics
|
|
||||||
- Protocol version configuration
|
|
||||||
- Custom JSON-RPC methods
|
|
||||||
|
|
||||||
## Code Assistance
|
|
||||||
|
|
||||||
I can help you with:
|
|
||||||
|
|
||||||
### Gemfile Setup
|
|
||||||
```ruby
|
|
||||||
gem 'mcp', '~> 0.4.0'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Server Creation
|
|
||||||
```ruby
|
|
||||||
server = MCP::Server.new(
|
|
||||||
name: 'my_server',
|
|
||||||
version: '1.0.0',
|
|
||||||
tools: [MyTool],
|
|
||||||
prompts: [MyPrompt],
|
|
||||||
server_context: { user_id: current_user.id }
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool Definition
|
|
||||||
```ruby
|
|
||||||
class MyTool < MCP::Tool
|
|
||||||
tool_name 'my_tool'
|
|
||||||
description 'Tool description'
|
|
||||||
|
|
||||||
input_schema(
|
|
||||||
properties: {
|
|
||||||
query: { type: 'string' }
|
|
||||||
},
|
|
||||||
required: ['query']
|
|
||||||
)
|
|
||||||
|
|
||||||
annotations(
|
|
||||||
read_only_hint: true
|
|
||||||
)
|
|
||||||
|
|
||||||
def self.call(query:, server_context:)
|
|
||||||
MCP::Tool::Response.new([{
|
|
||||||
type: 'text',
|
|
||||||
text: 'Result'
|
|
||||||
}])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stdio Transport
|
|
||||||
```ruby
|
|
||||||
transport = MCP::Server::Transports::StdioTransport.new(server)
|
|
||||||
transport.open
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rails Integration
|
|
||||||
```ruby
|
|
||||||
class McpController < ApplicationController
|
|
||||||
def index
|
|
||||||
server = MCP::Server.new(
|
|
||||||
name: 'rails_server',
|
|
||||||
tools: [MyTool],
|
|
||||||
server_context: { user_id: current_user.id }
|
|
||||||
)
|
|
||||||
render json: server.handle_json(request.body.read)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### Use Classes for Tools
|
|
||||||
Organize tools as classes for better structure:
|
|
||||||
```ruby
|
|
||||||
class GreetTool < MCP::Tool
|
|
||||||
tool_name 'greet'
|
|
||||||
description 'Generate greeting'
|
|
||||||
|
|
||||||
def self.call(name:, server_context:)
|
|
||||||
MCP::Tool::Response.new([{
|
|
||||||
type: 'text',
|
|
||||||
text: "Hello, #{name}!"
|
|
||||||
}])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Define Schemas
|
|
||||||
Ensure type safety with input/output schemas:
|
|
||||||
```ruby
|
|
||||||
input_schema(
|
|
||||||
properties: {
|
|
||||||
name: { type: 'string' },
|
|
||||||
age: { type: 'integer', minimum: 0 }
|
|
||||||
},
|
|
||||||
required: ['name']
|
|
||||||
)
|
|
||||||
|
|
||||||
output_schema(
|
|
||||||
properties: {
|
|
||||||
message: { type: 'string' },
|
|
||||||
timestamp: { type: 'string', format: 'date-time' }
|
|
||||||
},
|
|
||||||
required: ['message']
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add Annotations
|
|
||||||
Provide behavior hints:
|
|
||||||
```ruby
|
|
||||||
annotations(
|
|
||||||
read_only_hint: true,
|
|
||||||
destructive_hint: false,
|
|
||||||
idempotent_hint: true
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Include Structured Content
|
|
||||||
Return both text and structured data:
|
|
||||||
```ruby
|
|
||||||
data = { temperature: 72, condition: 'sunny' }
|
|
||||||
|
|
||||||
MCP::Tool::Response.new(
|
|
||||||
[{ type: 'text', text: data.to_json }],
|
|
||||||
structured_content: data
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
### Authenticated Tool
|
|
||||||
```ruby
|
|
||||||
class SecureTool < MCP::Tool
|
|
||||||
def self.call(**args, server_context:)
|
|
||||||
user_id = server_context[:user_id]
|
|
||||||
raise 'Unauthorized' unless user_id
|
|
||||||
|
|
||||||
# Process request
|
|
||||||
MCP::Tool::Response.new([{
|
|
||||||
type: 'text',
|
|
||||||
text: 'Success'
|
|
||||||
}])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
```ruby
|
|
||||||
def self.call(data:, server_context:)
|
|
||||||
begin
|
|
||||||
result = process(data)
|
|
||||||
MCP::Tool::Response.new([{
|
|
||||||
type: 'text',
|
|
||||||
text: result
|
|
||||||
}])
|
|
||||||
rescue ValidationError => e
|
|
||||||
MCP::Tool::Response.new(
|
|
||||||
[{ type: 'text', text: e.message }],
|
|
||||||
is_error: true
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource Handler
|
|
||||||
```ruby
|
|
||||||
server.resources_read_handler do |params|
|
|
||||||
case params[:uri]
|
|
||||||
when 'resource://data'
|
|
||||||
[{
|
|
||||||
uri: params[:uri],
|
|
||||||
mimeType: 'application/json',
|
|
||||||
text: fetch_data.to_json
|
|
||||||
}]
|
|
||||||
else
|
|
||||||
raise "Unknown resource: #{params[:uri]}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dynamic Prompt
|
|
||||||
```ruby
|
|
||||||
class CustomPrompt < MCP::Prompt
|
|
||||||
def self.template(args, server_context:)
|
|
||||||
user_id = server_context[:user_id]
|
|
||||||
user = User.find(user_id)
|
|
||||||
|
|
||||||
MCP::Prompt::Result.new(
|
|
||||||
description: "Prompt for #{user.name}",
|
|
||||||
messages: generate_for(user)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Exception Reporting
|
|
||||||
```ruby
|
|
||||||
MCP.configure do |config|
|
|
||||||
config.exception_reporter = ->(exception, context) {
|
|
||||||
Bugsnag.notify(exception) do |report|
|
|
||||||
report.add_metadata(:mcp, context)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Instrumentation
|
|
||||||
```ruby
|
|
||||||
MCP.configure do |config|
|
|
||||||
config.instrumentation_callback = ->(data) {
|
|
||||||
StatsD.timing("mcp.#{data[:method]}", data[:duration])
|
|
||||||
}
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Methods
|
|
||||||
```ruby
|
|
||||||
server.define_custom_method(method_name: 'custom') do |params|
|
|
||||||
# Return result or nil for notifications
|
|
||||||
{ status: 'ok' }
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Tool Tests
|
|
||||||
```ruby
|
|
||||||
class MyToolTest < Minitest::Test
|
|
||||||
def test_tool_call
|
|
||||||
response = MyTool.call(
|
|
||||||
query: 'test',
|
|
||||||
server_context: {}
|
|
||||||
)
|
|
||||||
|
|
||||||
refute response.is_error
|
|
||||||
assert_equal 1, response.content.length
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
```ruby
|
|
||||||
def test_server_handles_request
|
|
||||||
server = MCP::Server.new(
|
|
||||||
name: 'test',
|
|
||||||
tools: [MyTool]
|
|
||||||
)
|
|
||||||
|
|
||||||
request = {
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
id: '1',
|
|
||||||
method: 'tools/call',
|
|
||||||
params: {
|
|
||||||
name: 'my_tool',
|
|
||||||
arguments: { query: 'test' }
|
|
||||||
}
|
|
||||||
}.to_json
|
|
||||||
|
|
||||||
response = JSON.parse(server.handle_json(request))
|
|
||||||
assert response['result']
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ruby SDK Features
|
|
||||||
|
|
||||||
### Supported Methods
|
|
||||||
- `initialize` - Protocol initialization
|
|
||||||
- `ping` - Health check
|
|
||||||
- `tools/list` - List tools
|
|
||||||
- `tools/call` - Call tool
|
|
||||||
- `prompts/list` - List prompts
|
|
||||||
- `prompts/get` - Get prompt
|
|
||||||
- `resources/list` - List resources
|
|
||||||
- `resources/read` - Read resource
|
|
||||||
- `resources/templates/list` - List resource templates
|
|
||||||
|
|
||||||
### Notifications
|
|
||||||
- `notify_tools_list_changed`
|
|
||||||
- `notify_prompts_list_changed`
|
|
||||||
- `notify_resources_list_changed`
|
|
||||||
|
|
||||||
### Transport Support
|
|
||||||
- Stdio transport for CLI
|
|
||||||
- HTTP transport for web services
|
|
||||||
- Streamable HTTP with SSE
|
|
||||||
|
|
||||||
## Ask Me About
|
|
||||||
|
|
||||||
- Server setup and configuration
|
|
||||||
- Tool, prompt, and resource implementations
|
|
||||||
- Rails integration patterns
|
|
||||||
- Exception reporting and instrumentation
|
|
||||||
- Input/output schema design
|
|
||||||
- Tool annotations
|
|
||||||
- Structured content responses
|
|
||||||
- Server context usage
|
|
||||||
- Testing strategies
|
|
||||||
- HTTP transport with authorization
|
|
||||||
- Custom JSON-RPC methods
|
|
||||||
- Notifications and list changes
|
|
||||||
- Protocol version management
|
|
||||||
- Performance optimization
|
|
||||||
|
|
||||||
I'm here to help you build idiomatic, production-ready Ruby MCP servers. What would you like to work on?
|
|
||||||
@ -1,660 +0,0 @@
|
|||||||
---
|
|
||||||
description: 'Generate a complete Model Context Protocol server project in Ruby using the official MCP Ruby SDK gem.'
|
|
||||||
mode: agent
|
|
||||||
---
|
|
||||||
|
|
||||||
# Ruby MCP Server Generator
|
|
||||||
|
|
||||||
Generate a complete, production-ready MCP server in Ruby using the official Ruby SDK.
|
|
||||||
|
|
||||||
## Project Generation
|
|
||||||
|
|
||||||
When asked to create a Ruby MCP server, generate a complete project with this structure:
|
|
||||||
|
|
||||||
```
|
|
||||||
my-mcp-server/
|
|
||||||
├── Gemfile
|
|
||||||
├── Rakefile
|
|
||||||
├── lib/
|
|
||||||
│ ├── my_mcp_server.rb
|
|
||||||
│ ├── my_mcp_server/
|
|
||||||
│ │ ├── server.rb
|
|
||||||
│ │ ├── tools/
|
|
||||||
│ │ │ ├── greet_tool.rb
|
|
||||||
│ │ │ └── calculate_tool.rb
|
|
||||||
│ │ ├── prompts/
|
|
||||||
│ │ │ └── code_review_prompt.rb
|
|
||||||
│ │ └── resources/
|
|
||||||
│ │ └── example_resource.rb
|
|
||||||
├── bin/
|
|
||||||
│ └── mcp-server
|
|
||||||
├── test/
|
|
||||||
│ ├── test_helper.rb
|
|
||||||
│ └── tools/
|
|
||||||
│ ├── greet_tool_test.rb
|
|
||||||
│ └── calculate_tool_test.rb
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Gemfile Template
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
source 'https://rubygems.org'
|
|
||||||
|
|
||||||
gem 'mcp', '~> 0.4.0'
|
|
||||||
|
|
||||||
group :development, :test do
|
|
||||||
gem 'minitest', '~> 5.0'
|
|
||||||
gem 'rake', '~> 13.0'
|
|
||||||
gem 'rubocop', '~> 1.50'
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rakefile Template
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
require 'rake/testtask'
|
|
||||||
require 'rubocop/rake_task'
|
|
||||||
|
|
||||||
Rake::TestTask.new(:test) do |t|
|
|
||||||
t.libs << 'test'
|
|
||||||
t.libs << 'lib'
|
|
||||||
t.test_files = FileList['test/**/*_test.rb']
|
|
||||||
end
|
|
||||||
|
|
||||||
RuboCop::RakeTask.new
|
|
||||||
|
|
||||||
task default: %i[test rubocop]
|
|
||||||
```
|
|
||||||
|
|
||||||
## lib/my_mcp_server.rb Template
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'mcp'
|
|
||||||
require_relative 'my_mcp_server/server'
|
|
||||||
require_relative 'my_mcp_server/tools/greet_tool'
|
|
||||||
require_relative 'my_mcp_server/tools/calculate_tool'
|
|
||||||
require_relative 'my_mcp_server/prompts/code_review_prompt'
|
|
||||||
require_relative 'my_mcp_server/resources/example_resource'
|
|
||||||
|
|
||||||
module MyMcpServer
|
|
||||||
VERSION = '1.0.0'
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## lib/my_mcp_server/server.rb Template
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module MyMcpServer
|
|
||||||
class Server
|
|
||||||
attr_reader :mcp_server
|
|
||||||
|
|
||||||
def initialize(server_context: {})
|
|
||||||
@mcp_server = MCP::Server.new(
|
|
||||||
name: 'my_mcp_server',
|
|
||||||
version: MyMcpServer::VERSION,
|
|
||||||
tools: [
|
|
||||||
Tools::GreetTool,
|
|
||||||
Tools::CalculateTool
|
|
||||||
],
|
|
||||||
prompts: [
|
|
||||||
Prompts::CodeReviewPrompt
|
|
||||||
],
|
|
||||||
resources: [
|
|
||||||
Resources::ExampleResource.resource
|
|
||||||
],
|
|
||||||
server_context: server_context
|
|
||||||
)
|
|
||||||
|
|
||||||
setup_resource_handler
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_json(json_string)
|
|
||||||
mcp_server.handle_json(json_string)
|
|
||||||
end
|
|
||||||
|
|
||||||
def start_stdio
|
|
||||||
transport = MCP::Server::Transports::StdioTransport.new(mcp_server)
|
|
||||||
transport.open
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def setup_resource_handler
|
|
||||||
mcp_server.resources_read_handler do |params|
|
|
||||||
Resources::ExampleResource.read(params[:uri])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## lib/my_mcp_server/tools/greet_tool.rb Template
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module MyMcpServer
|
|
||||||
module Tools
|
|
||||||
class GreetTool < MCP::Tool
|
|
||||||
tool_name 'greet'
|
|
||||||
description 'Generate a greeting message'
|
|
||||||
|
|
||||||
input_schema(
|
|
||||||
properties: {
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Name to greet'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ['name']
|
|
||||||
)
|
|
||||||
|
|
||||||
output_schema(
|
|
||||||
properties: {
|
|
||||||
message: { type: 'string' },
|
|
||||||
timestamp: { type: 'string', format: 'date-time' }
|
|
||||||
},
|
|
||||||
required: ['message', 'timestamp']
|
|
||||||
)
|
|
||||||
|
|
||||||
annotations(
|
|
||||||
read_only_hint: true,
|
|
||||||
idempotent_hint: true
|
|
||||||
)
|
|
||||||
|
|
||||||
def self.call(name:, server_context:)
|
|
||||||
timestamp = Time.now.iso8601
|
|
||||||
message = "Hello, #{name}! Welcome to MCP."
|
|
||||||
|
|
||||||
structured_data = {
|
|
||||||
message: message,
|
|
||||||
timestamp: timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
MCP::Tool::Response.new(
|
|
||||||
[{ type: 'text', text: message }],
|
|
||||||
structured_content: structured_data
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## lib/my_mcp_server/tools/calculate_tool.rb Template
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module MyMcpServer
|
|
||||||
module Tools
|
|
||||||
class CalculateTool < MCP::Tool
|
|
||||||
tool_name 'calculate'
|
|
||||||
description 'Perform mathematical calculations'
|
|
||||||
|
|
||||||
input_schema(
|
|
||||||
properties: {
|
|
||||||
operation: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Operation to perform',
|
|
||||||
enum: ['add', 'subtract', 'multiply', 'divide']
|
|
||||||
},
|
|
||||||
a: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'First operand'
|
|
||||||
},
|
|
||||||
b: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Second operand'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ['operation', 'a', 'b']
|
|
||||||
)
|
|
||||||
|
|
||||||
output_schema(
|
|
||||||
properties: {
|
|
||||||
result: { type: 'number' },
|
|
||||||
operation: { type: 'string' }
|
|
||||||
},
|
|
||||||
required: ['result', 'operation']
|
|
||||||
)
|
|
||||||
|
|
||||||
annotations(
|
|
||||||
read_only_hint: true,
|
|
||||||
idempotent_hint: true
|
|
||||||
)
|
|
||||||
|
|
||||||
def self.call(operation:, a:, b:, server_context:)
|
|
||||||
result = case operation
|
|
||||||
when 'add' then a + b
|
|
||||||
when 'subtract' then a - b
|
|
||||||
when 'multiply' then a * b
|
|
||||||
when 'divide'
|
|
||||||
return error_response('Division by zero') if b.zero?
|
|
||||||
a / b.to_f
|
|
||||||
else
|
|
||||||
return error_response("Unknown operation: #{operation}")
|
|
||||||
end
|
|
||||||
|
|
||||||
structured_data = {
|
|
||||||
result: result,
|
|
||||||
operation: operation
|
|
||||||
}
|
|
||||||
|
|
||||||
MCP::Tool::Response.new(
|
|
||||||
[{ type: 'text', text: "Result: #{result}" }],
|
|
||||||
structured_content: structured_data
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.error_response(message)
|
|
||||||
MCP::Tool::Response.new(
|
|
||||||
[{ type: 'text', text: message }],
|
|
||||||
is_error: true
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## lib/my_mcp_server/prompts/code_review_prompt.rb Template
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module MyMcpServer
|
|
||||||
module Prompts
|
|
||||||
class CodeReviewPrompt < MCP::Prompt
|
|
||||||
prompt_name 'code_review'
|
|
||||||
description 'Generate a code review prompt'
|
|
||||||
|
|
||||||
arguments [
|
|
||||||
MCP::Prompt::Argument.new(
|
|
||||||
name: 'language',
|
|
||||||
description: 'Programming language',
|
|
||||||
required: true
|
|
||||||
),
|
|
||||||
MCP::Prompt::Argument.new(
|
|
||||||
name: 'focus',
|
|
||||||
description: 'Review focus area (e.g., performance, security)',
|
|
||||||
required: false
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
meta(
|
|
||||||
version: '1.0',
|
|
||||||
category: 'development'
|
|
||||||
)
|
|
||||||
|
|
||||||
def self.template(args, server_context:)
|
|
||||||
language = args['language'] || 'Ruby'
|
|
||||||
focus = args['focus'] || 'general quality'
|
|
||||||
|
|
||||||
MCP::Prompt::Result.new(
|
|
||||||
description: "Code review for #{language} with focus on #{focus}",
|
|
||||||
messages: [
|
|
||||||
MCP::Prompt::Message.new(
|
|
||||||
role: 'user',
|
|
||||||
content: MCP::Content::Text.new(
|
|
||||||
"Please review this #{language} code with focus on #{focus}."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
MCP::Prompt::Message.new(
|
|
||||||
role: 'assistant',
|
|
||||||
content: MCP::Content::Text.new(
|
|
||||||
"I'll review the code focusing on #{focus}. Please share the code."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
MCP::Prompt::Message.new(
|
|
||||||
role: 'user',
|
|
||||||
content: MCP::Content::Text.new(
|
|
||||||
'[paste code here]'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## lib/my_mcp_server/resources/example_resource.rb Template
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module MyMcpServer
|
|
||||||
module Resources
|
|
||||||
class ExampleResource
|
|
||||||
RESOURCE_URI = 'resource://data/example'
|
|
||||||
|
|
||||||
def self.resource
|
|
||||||
MCP::Resource.new(
|
|
||||||
uri: RESOURCE_URI,
|
|
||||||
name: 'example-data',
|
|
||||||
description: 'Example resource data',
|
|
||||||
mime_type: 'application/json'
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.read(uri)
|
|
||||||
return [] unless uri == RESOURCE_URI
|
|
||||||
|
|
||||||
data = {
|
|
||||||
message: 'Example resource data',
|
|
||||||
timestamp: Time.now.iso8601,
|
|
||||||
version: MyMcpServer::VERSION
|
|
||||||
}
|
|
||||||
|
|
||||||
[{
|
|
||||||
uri: uri,
|
|
||||||
mimeType: 'application/json',
|
|
||||||
text: data.to_json
|
|
||||||
}]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## bin/mcp-server Template
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
#!/usr/bin/env ruby
|
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require_relative '../lib/my_mcp_server'
|
|
||||||
|
|
||||||
begin
|
|
||||||
server = MyMcpServer::Server.new
|
|
||||||
server.start_stdio
|
|
||||||
rescue Interrupt
|
|
||||||
warn "\nShutting down server..."
|
|
||||||
exit 0
|
|
||||||
rescue StandardError => e
|
|
||||||
warn "Error: #{e.message}"
|
|
||||||
warn e.backtrace.join("\n")
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Make the file executable:
|
|
||||||
```bash
|
|
||||||
chmod +x bin/mcp-server
|
|
||||||
```
|
|
||||||
|
|
||||||
## test/test_helper.rb Template
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
||||||
require 'my_mcp_server'
|
|
||||||
require 'minitest/autorun'
|
|
||||||
```
|
|
||||||
|
|
||||||
## test/tools/greet_tool_test.rb Template
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'test_helper'
|
|
||||||
|
|
||||||
module MyMcpServer
|
|
||||||
module Tools
|
|
||||||
class GreetToolTest < Minitest::Test
|
|
||||||
def test_greet_with_name
|
|
||||||
response = GreetTool.call(
|
|
||||||
name: 'Ruby',
|
|
||||||
server_context: {}
|
|
||||||
)
|
|
||||||
|
|
||||||
refute response.is_error
|
|
||||||
assert_equal 1, response.content.length
|
|
||||||
assert_match(/Ruby/, response.content.first[:text])
|
|
||||||
|
|
||||||
assert response.structured_content
|
|
||||||
assert_equal 'Hello, Ruby! Welcome to MCP.', response.structured_content[:message]
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_output_schema_validation
|
|
||||||
response = GreetTool.call(
|
|
||||||
name: 'Test',
|
|
||||||
server_context: {}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.structured_content.key?(:message)
|
|
||||||
assert response.structured_content.key?(:timestamp)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## test/tools/calculate_tool_test.rb Template
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'test_helper'
|
|
||||||
|
|
||||||
module MyMcpServer
|
|
||||||
module Tools
|
|
||||||
class CalculateToolTest < Minitest::Test
|
|
||||||
def test_addition
|
|
||||||
response = CalculateTool.call(
|
|
||||||
operation: 'add',
|
|
||||||
a: 5,
|
|
||||||
b: 3,
|
|
||||||
server_context: {}
|
|
||||||
)
|
|
||||||
|
|
||||||
refute response.is_error
|
|
||||||
assert_equal 8, response.structured_content[:result]
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_subtraction
|
|
||||||
response = CalculateTool.call(
|
|
||||||
operation: 'subtract',
|
|
||||||
a: 10,
|
|
||||||
b: 4,
|
|
||||||
server_context: {}
|
|
||||||
)
|
|
||||||
|
|
||||||
refute response.is_error
|
|
||||||
assert_equal 6, response.structured_content[:result]
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_multiplication
|
|
||||||
response = CalculateTool.call(
|
|
||||||
operation: 'multiply',
|
|
||||||
a: 6,
|
|
||||||
b: 7,
|
|
||||||
server_context: {}
|
|
||||||
)
|
|
||||||
|
|
||||||
refute response.is_error
|
|
||||||
assert_equal 42, response.structured_content[:result]
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_division
|
|
||||||
response = CalculateTool.call(
|
|
||||||
operation: 'divide',
|
|
||||||
a: 15,
|
|
||||||
b: 3,
|
|
||||||
server_context: {}
|
|
||||||
)
|
|
||||||
|
|
||||||
refute response.is_error
|
|
||||||
assert_equal 5.0, response.structured_content[:result]
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_division_by_zero
|
|
||||||
response = CalculateTool.call(
|
|
||||||
operation: 'divide',
|
|
||||||
a: 10,
|
|
||||||
b: 0,
|
|
||||||
server_context: {}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.is_error
|
|
||||||
assert_match(/Division by zero/, response.content.first[:text])
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_unknown_operation
|
|
||||||
response = CalculateTool.call(
|
|
||||||
operation: 'modulo',
|
|
||||||
a: 10,
|
|
||||||
b: 3,
|
|
||||||
server_context: {}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.is_error
|
|
||||||
assert_match(/Unknown operation/, response.content.first[:text])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## README.md Template
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# My MCP Server
|
|
||||||
|
|
||||||
A Model Context Protocol server built with Ruby and the official MCP Ruby SDK.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- ✅ Tools: greet, calculate
|
|
||||||
- ✅ Prompts: code_review
|
|
||||||
- ✅ Resources: example-data
|
|
||||||
- ✅ Input/output schemas
|
|
||||||
- ✅ Tool annotations
|
|
||||||
- ✅ Structured content
|
|
||||||
- ✅ Full test coverage
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Ruby 3.0 or later
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bundle install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Stdio Transport
|
|
||||||
|
|
||||||
Run the server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bundle exec bin/mcp-server
|
|
||||||
```
|
|
||||||
|
|
||||||
Then send JSON-RPC requests:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
{"jsonrpc":"2.0","id":"1","method":"ping"}
|
|
||||||
{"jsonrpc":"2.0","id":"2","method":"tools/list"}
|
|
||||||
{"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"greet","arguments":{"name":"Ruby"}}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rails Integration
|
|
||||||
|
|
||||||
Add to your Rails controller:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class McpController < ApplicationController
|
|
||||||
def index
|
|
||||||
server = MyMcpServer::Server.new(
|
|
||||||
server_context: { user_id: current_user.id }
|
|
||||||
)
|
|
||||||
render json: server.handle_json(request.body.read)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Run tests:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bundle exec rake test
|
|
||||||
```
|
|
||||||
|
|
||||||
Run linter:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bundle exec rake rubocop
|
|
||||||
```
|
|
||||||
|
|
||||||
Run all checks:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bundle exec rake
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration with Claude Desktop
|
|
||||||
|
|
||||||
Add to `claude_desktop_config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"my-mcp-server": {
|
|
||||||
"command": "bundle",
|
|
||||||
"args": ["exec", "bin/mcp-server"],
|
|
||||||
"cwd": "/path/to/my-mcp-server"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
my-mcp-server/
|
|
||||||
├── Gemfile # Dependencies
|
|
||||||
├── Rakefile # Build tasks
|
|
||||||
├── lib/ # Source code
|
|
||||||
│ ├── my_mcp_server.rb # Main entry point
|
|
||||||
│ └── my_mcp_server/ # Module namespace
|
|
||||||
│ ├── server.rb # Server setup
|
|
||||||
│ ├── tools/ # Tool implementations
|
|
||||||
│ ├── prompts/ # Prompt templates
|
|
||||||
│ └── resources/ # Resource handlers
|
|
||||||
├── bin/ # Executables
|
|
||||||
│ └── mcp-server # Stdio server
|
|
||||||
├── test/ # Test suite
|
|
||||||
│ ├── test_helper.rb # Test configuration
|
|
||||||
│ └── tools/ # Tool tests
|
|
||||||
└── README.md # This file
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
```
|
|
||||||
|
|
||||||
## Generation Instructions
|
|
||||||
|
|
||||||
1. **Ask for project name and description**
|
|
||||||
2. **Generate all files** with proper naming and module structure
|
|
||||||
3. **Use classes for tools and prompts** for better organization
|
|
||||||
4. **Include input/output schemas** for type safety
|
|
||||||
5. **Add tool annotations** for behavior hints
|
|
||||||
6. **Include structured content** in responses
|
|
||||||
7. **Implement comprehensive tests** for all tools
|
|
||||||
8. **Follow Ruby conventions** (snake_case, modules, frozen_string_literal)
|
|
||||||
9. **Add proper error handling** with is_error flag
|
|
||||||
10. **Provide both stdio and HTTP** usage examples
|
|
||||||
@ -1,629 +0,0 @@
|
|||||||
---
|
|
||||||
description: 'Best practices and patterns for building Model Context Protocol (MCP) servers in Ruby using the official MCP Ruby SDK gem.'
|
|
||||||
applyTo: "**/*.rb, **/Gemfile, **/*.gemspec, **/Rakefile"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Ruby MCP Server Development Guidelines
|
|
||||||
|
|
||||||
When building MCP servers in Ruby, follow these best practices and patterns using the official Ruby SDK.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Add the MCP gem to your Gemfile:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
gem 'mcp'
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bundle install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server Setup
|
|
||||||
|
|
||||||
Create an MCP server instance:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
require 'mcp'
|
|
||||||
|
|
||||||
server = MCP::Server.new(
|
|
||||||
name: 'my_server',
|
|
||||||
version: '1.0.0'
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding Tools
|
|
||||||
|
|
||||||
Define tools using classes or blocks:
|
|
||||||
|
|
||||||
### Tool as Class
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class GreetTool < MCP::Tool
|
|
||||||
tool_name 'greet'
|
|
||||||
description 'Generate a greeting message'
|
|
||||||
|
|
||||||
input_schema(
|
|
||||||
properties: {
|
|
||||||
name: { type: 'string', description: 'Name to greet' }
|
|
||||||
},
|
|
||||||
required: ['name']
|
|
||||||
)
|
|
||||||
|
|
||||||
output_schema(
|
|
||||||
properties: {
|
|
||||||
message: { type: 'string' },
|
|
||||||
timestamp: { type: 'string', format: 'date-time' }
|
|
||||||
},
|
|
||||||
required: ['message']
|
|
||||||
)
|
|
||||||
|
|
||||||
annotations(
|
|
||||||
read_only_hint: true,
|
|
||||||
idempotent_hint: true
|
|
||||||
)
|
|
||||||
|
|
||||||
def self.call(name:, server_context:)
|
|
||||||
MCP::Tool::Response.new([{
|
|
||||||
type: 'text',
|
|
||||||
text: "Hello, #{name}! Welcome to MCP."
|
|
||||||
}], structured_content: {
|
|
||||||
message: "Hello, #{name}!",
|
|
||||||
timestamp: Time.now.iso8601
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
server = MCP::Server.new(
|
|
||||||
name: 'my_server',
|
|
||||||
tools: [GreetTool]
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool with Block
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
server.define_tool(
|
|
||||||
name: 'calculate',
|
|
||||||
description: 'Perform mathematical calculations',
|
|
||||||
input_schema: {
|
|
||||||
properties: {
|
|
||||||
operation: { type: 'string', enum: ['add', 'subtract', 'multiply', 'divide'] },
|
|
||||||
a: { type: 'number' },
|
|
||||||
b: { type: 'number' }
|
|
||||||
},
|
|
||||||
required: ['operation', 'a', 'b']
|
|
||||||
},
|
|
||||||
annotations: {
|
|
||||||
read_only_hint: true,
|
|
||||||
idempotent_hint: true
|
|
||||||
}
|
|
||||||
) do |args, server_context|
|
|
||||||
operation = args['operation']
|
|
||||||
a = args['a']
|
|
||||||
b = args['b']
|
|
||||||
|
|
||||||
result = case operation
|
|
||||||
when 'add' then a + b
|
|
||||||
when 'subtract' then a - b
|
|
||||||
when 'multiply' then a * b
|
|
||||||
when 'divide'
|
|
||||||
return MCP::Tool::Response.new([{ type: 'text', text: 'Division by zero' }], is_error: true) if b == 0
|
|
||||||
a / b
|
|
||||||
else
|
|
||||||
return MCP::Tool::Response.new([{ type: 'text', text: "Unknown operation: #{operation}" }], is_error: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
MCP::Tool::Response.new([{ type: 'text', text: "Result: #{result}" }])
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding Resources
|
|
||||||
|
|
||||||
Define resources for data access:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# Register resources
|
|
||||||
resource = MCP::Resource.new(
|
|
||||||
uri: 'resource://data/example',
|
|
||||||
name: 'example-data',
|
|
||||||
description: 'Example resource data',
|
|
||||||
mime_type: 'application/json'
|
|
||||||
)
|
|
||||||
|
|
||||||
server = MCP::Server.new(
|
|
||||||
name: 'my_server',
|
|
||||||
resources: [resource]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Define read handler
|
|
||||||
server.resources_read_handler do |params|
|
|
||||||
case params[:uri]
|
|
||||||
when 'resource://data/example'
|
|
||||||
[{
|
|
||||||
uri: params[:uri],
|
|
||||||
mimeType: 'application/json',
|
|
||||||
text: { message: 'Example data', timestamp: Time.now }.to_json
|
|
||||||
}]
|
|
||||||
else
|
|
||||||
raise "Unknown resource: #{params[:uri]}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding Prompts
|
|
||||||
|
|
||||||
Define prompt templates:
|
|
||||||
|
|
||||||
### Prompt as Class
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class CodeReviewPrompt < MCP::Prompt
|
|
||||||
prompt_name 'code_review'
|
|
||||||
description 'Generate a code review prompt'
|
|
||||||
|
|
||||||
arguments [
|
|
||||||
MCP::Prompt::Argument.new(
|
|
||||||
name: 'language',
|
|
||||||
description: 'Programming language',
|
|
||||||
required: true
|
|
||||||
),
|
|
||||||
MCP::Prompt::Argument.new(
|
|
||||||
name: 'focus',
|
|
||||||
description: 'Review focus area',
|
|
||||||
required: false
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
def self.template(args, server_context:)
|
|
||||||
language = args['language'] || 'Ruby'
|
|
||||||
focus = args['focus'] || 'general quality'
|
|
||||||
|
|
||||||
MCP::Prompt::Result.new(
|
|
||||||
description: "Code review for #{language} with focus on #{focus}",
|
|
||||||
messages: [
|
|
||||||
MCP::Prompt::Message.new(
|
|
||||||
role: 'user',
|
|
||||||
content: MCP::Content::Text.new("Please review this #{language} code with focus on #{focus}.")
|
|
||||||
),
|
|
||||||
MCP::Prompt::Message.new(
|
|
||||||
role: 'assistant',
|
|
||||||
content: MCP::Content::Text.new("I'll review the code focusing on #{focus}. Please share the code.")
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
server = MCP::Server.new(
|
|
||||||
name: 'my_server',
|
|
||||||
prompts: [CodeReviewPrompt]
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Prompt with Block
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
server.define_prompt(
|
|
||||||
name: 'analyze',
|
|
||||||
description: 'Analyze a topic',
|
|
||||||
arguments: [
|
|
||||||
MCP::Prompt::Argument.new(name: 'topic', description: 'Topic to analyze', required: true),
|
|
||||||
MCP::Prompt::Argument.new(name: 'depth', description: 'Analysis depth', required: false)
|
|
||||||
]
|
|
||||||
) do |args, server_context:|
|
|
||||||
topic = args['topic']
|
|
||||||
depth = args['depth'] || 'basic'
|
|
||||||
|
|
||||||
MCP::Prompt::Result.new(
|
|
||||||
description: "Analysis of #{topic} at #{depth} level",
|
|
||||||
messages: [
|
|
||||||
MCP::Prompt::Message.new(
|
|
||||||
role: 'user',
|
|
||||||
content: MCP::Content::Text.new("Please analyze: #{topic}")
|
|
||||||
),
|
|
||||||
MCP::Prompt::Message.new(
|
|
||||||
role: 'assistant',
|
|
||||||
content: MCP::Content::Text.new("I'll provide a #{depth} analysis of #{topic}")
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Transport Configuration
|
|
||||||
|
|
||||||
### Stdio Transport
|
|
||||||
|
|
||||||
For local command-line applications:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
require 'mcp'
|
|
||||||
|
|
||||||
server = MCP::Server.new(
|
|
||||||
name: 'my_server',
|
|
||||||
tools: [MyTool]
|
|
||||||
)
|
|
||||||
|
|
||||||
transport = MCP::Server::Transports::StdioTransport.new(server)
|
|
||||||
transport.open
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTTP Transport (Rails)
|
|
||||||
|
|
||||||
For Rails applications:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class McpController < ApplicationController
|
|
||||||
def index
|
|
||||||
server = MCP::Server.new(
|
|
||||||
name: 'rails_server',
|
|
||||||
version: '1.0.0',
|
|
||||||
tools: [SomeTool],
|
|
||||||
prompts: [MyPrompt],
|
|
||||||
server_context: { user_id: current_user.id }
|
|
||||||
)
|
|
||||||
|
|
||||||
render json: server.handle_json(request.body.read)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Streamable HTTP Transport
|
|
||||||
|
|
||||||
For Server-Sent Events:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
server = MCP::Server.new(name: 'my_server')
|
|
||||||
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
|
|
||||||
server.transport = transport
|
|
||||||
|
|
||||||
# When tools change, notify clients
|
|
||||||
server.define_tool(name: 'new_tool') { |**args| { result: 'ok' } }
|
|
||||||
server.notify_tools_list_changed
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server Context
|
|
||||||
|
|
||||||
Pass contextual information to tools and prompts:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
server = MCP::Server.new(
|
|
||||||
name: 'my_server',
|
|
||||||
tools: [AuthenticatedTool],
|
|
||||||
server_context: {
|
|
||||||
user_id: current_user.id,
|
|
||||||
request_id: request.uuid,
|
|
||||||
auth_token: session[:token]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
class AuthenticatedTool < MCP::Tool
|
|
||||||
def self.call(query:, server_context:)
|
|
||||||
user_id = server_context[:user_id]
|
|
||||||
# Use user_id for authorization
|
|
||||||
|
|
||||||
MCP::Tool::Response.new([{ type: 'text', text: 'Authorized' }])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Exception Reporting
|
|
||||||
|
|
||||||
Configure exception reporting:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
MCP.configure do |config|
|
|
||||||
config.exception_reporter = ->(exception, server_context) {
|
|
||||||
# Report to your error tracking service
|
|
||||||
Bugsnag.notify(exception) do |report|
|
|
||||||
report.add_metadata(:mcp, server_context)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Instrumentation
|
|
||||||
|
|
||||||
Monitor MCP server performance:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
MCP.configure do |config|
|
|
||||||
config.instrumentation_callback = ->(data) {
|
|
||||||
# Log instrumentation data
|
|
||||||
Rails.logger.info("MCP: #{data.inspect}")
|
|
||||||
|
|
||||||
# Or send to metrics service
|
|
||||||
StatsD.timing("mcp.#{data[:method]}.duration", data[:duration])
|
|
||||||
StatsD.increment("mcp.#{data[:method]}.count")
|
|
||||||
}
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
The instrumentation data includes:
|
|
||||||
- `method`: Protocol method called (e.g., "tools/call")
|
|
||||||
- `tool_name`: Name of tool called
|
|
||||||
- `prompt_name`: Name of prompt called
|
|
||||||
- `resource_uri`: URI of resource called
|
|
||||||
- `error`: Error code if lookup failed
|
|
||||||
- `duration`: Duration in seconds
|
|
||||||
|
|
||||||
### Protocol Version
|
|
||||||
|
|
||||||
Override the protocol version:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
configuration = MCP::Configuration.new(protocol_version: '2025-06-18')
|
|
||||||
server = MCP::Server.new(name: 'my_server', configuration: configuration)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tool Annotations
|
|
||||||
|
|
||||||
Provide metadata about tool behavior:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class DataTool < MCP::Tool
|
|
||||||
annotations(
|
|
||||||
read_only_hint: true, # Tool only reads data
|
|
||||||
destructive_hint: false, # Tool doesn't destroy data
|
|
||||||
idempotent_hint: true, # Same input = same output
|
|
||||||
open_world_hint: false # Tool operates in closed context
|
|
||||||
)
|
|
||||||
|
|
||||||
def self.call(**args, server_context:)
|
|
||||||
# Implementation
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tool Output Schemas
|
|
||||||
|
|
||||||
Define expected output structure:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class WeatherTool < MCP::Tool
|
|
||||||
output_schema(
|
|
||||||
properties: {
|
|
||||||
temperature: { type: 'number' },
|
|
||||||
condition: { type: 'string' },
|
|
||||||
humidity: { type: 'integer' }
|
|
||||||
},
|
|
||||||
required: ['temperature', 'condition']
|
|
||||||
)
|
|
||||||
|
|
||||||
def self.call(location:, server_context:)
|
|
||||||
weather_data = {
|
|
||||||
temperature: 72.5,
|
|
||||||
condition: 'sunny',
|
|
||||||
humidity: 45
|
|
||||||
}
|
|
||||||
|
|
||||||
# Validate against schema
|
|
||||||
output_schema.validate_result(weather_data)
|
|
||||||
|
|
||||||
MCP::Tool::Response.new(
|
|
||||||
[{ type: 'text', text: weather_data.to_json }],
|
|
||||||
structured_content: weather_data
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Structured Content in Responses
|
|
||||||
|
|
||||||
Return structured data with text:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class APITool < MCP::Tool
|
|
||||||
def self.call(endpoint:, server_context:)
|
|
||||||
api_data = call_api(endpoint)
|
|
||||||
|
|
||||||
MCP::Tool::Response.new(
|
|
||||||
[{ type: 'text', text: api_data.to_json }],
|
|
||||||
structured_content: api_data
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Custom Methods
|
|
||||||
|
|
||||||
Define custom JSON-RPC methods:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
server = MCP::Server.new(name: 'my_server')
|
|
||||||
|
|
||||||
# Custom method with result
|
|
||||||
server.define_custom_method(method_name: 'add') do |params|
|
|
||||||
params[:a] + params[:b]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Custom notification (returns nil)
|
|
||||||
server.define_custom_method(method_name: 'notify') do |params|
|
|
||||||
puts "Notification: #{params[:message]}"
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notifications
|
|
||||||
|
|
||||||
Send list change notifications:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
server = MCP::Server.new(name: 'my_server')
|
|
||||||
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
|
|
||||||
server.transport = transport
|
|
||||||
|
|
||||||
# Notify when tools change
|
|
||||||
server.define_tool(name: 'new_tool') { |**args| { result: 'ok' } }
|
|
||||||
server.notify_tools_list_changed
|
|
||||||
|
|
||||||
# Notify when prompts change
|
|
||||||
server.define_prompt(name: 'new_prompt') { |args, **_| MCP::Prompt::Result.new(...) }
|
|
||||||
server.notify_prompts_list_changed
|
|
||||||
|
|
||||||
# Notify when resources change
|
|
||||||
server.notify_resources_list_changed
|
|
||||||
```
|
|
||||||
|
|
||||||
## Resource Templates
|
|
||||||
|
|
||||||
Define dynamic resources with URI templates:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
resource_template = MCP::ResourceTemplate.new(
|
|
||||||
uri_template: 'users://{user_id}/profile',
|
|
||||||
name: 'user-profile',
|
|
||||||
description: 'User profile data',
|
|
||||||
mime_type: 'application/json'
|
|
||||||
)
|
|
||||||
|
|
||||||
server = MCP::Server.new(
|
|
||||||
name: 'my_server',
|
|
||||||
resource_templates: [resource_template]
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
Handle errors properly in tools:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class RiskyTool < MCP::Tool
|
|
||||||
def self.call(data:, server_context:)
|
|
||||||
begin
|
|
||||||
result = risky_operation(data)
|
|
||||||
MCP::Tool::Response.new([{ type: 'text', text: result }])
|
|
||||||
rescue ValidationError => e
|
|
||||||
MCP::Tool::Response.new(
|
|
||||||
[{ type: 'text', text: "Invalid input: #{e.message}" }],
|
|
||||||
is_error: true
|
|
||||||
)
|
|
||||||
rescue => e
|
|
||||||
# Will be caught and reported by exception_reporter
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Write tests for your MCP server:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
require 'minitest/autorun'
|
|
||||||
require 'mcp'
|
|
||||||
|
|
||||||
class MyToolTest < Minitest::Test
|
|
||||||
def test_greet_tool
|
|
||||||
response = GreetTool.call(name: 'Ruby', server_context: {})
|
|
||||||
|
|
||||||
assert_equal 1, response.content.length
|
|
||||||
assert_match(/Ruby/, response.content.first[:text])
|
|
||||||
refute response.is_error
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_invalid_input
|
|
||||||
response = CalculateTool.call(operation: 'divide', a: 10, b: 0, server_context: {})
|
|
||||||
|
|
||||||
assert response.is_error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Client Usage
|
|
||||||
|
|
||||||
Build MCP clients to connect to servers:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
require 'mcp'
|
|
||||||
require 'faraday'
|
|
||||||
|
|
||||||
# HTTP transport
|
|
||||||
http_transport = MCP::Client::HTTP.new(
|
|
||||||
url: 'https://api.example.com/mcp',
|
|
||||||
headers: { 'Authorization' => "Bearer #{token}" }
|
|
||||||
)
|
|
||||||
|
|
||||||
client = MCP::Client.new(transport: http_transport)
|
|
||||||
|
|
||||||
# List tools
|
|
||||||
tools = client.tools
|
|
||||||
tools.each do |tool|
|
|
||||||
puts "Tool: #{tool.name}"
|
|
||||||
puts "Description: #{tool.description}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Call a tool
|
|
||||||
response = client.call_tool(
|
|
||||||
tool: tools.first,
|
|
||||||
arguments: { message: 'Hello, world!' }
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Use classes for complex tools** - Better organization and testability
|
|
||||||
2. **Define input/output schemas** - Ensure type safety and validation
|
|
||||||
3. **Add annotations** - Help clients understand tool behavior
|
|
||||||
4. **Include structured content** - Provide both text and structured data
|
|
||||||
5. **Use server_context** - Pass authentication and request context
|
|
||||||
6. **Configure exception reporting** - Monitor errors in production
|
|
||||||
7. **Implement instrumentation** - Track performance metrics
|
|
||||||
8. **Send notifications** - Keep clients updated on changes
|
|
||||||
9. **Validate inputs** - Check parameters before processing
|
|
||||||
10. **Follow Ruby conventions** - Use snake_case, proper indentation
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
### Authenticated Tool
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class AuthenticatedTool < MCP::Tool
|
|
||||||
def self.call(**args, server_context:)
|
|
||||||
user_id = server_context[:user_id]
|
|
||||||
raise 'Unauthorized' unless user_id
|
|
||||||
|
|
||||||
# Process authenticated request
|
|
||||||
MCP::Tool::Response.new([{ type: 'text', text: 'Success' }])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Paginated Resource
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
server.resources_read_handler do |params|
|
|
||||||
uri = params[:uri]
|
|
||||||
page = params[:page] || 1
|
|
||||||
|
|
||||||
data = fetch_paginated_data(page)
|
|
||||||
|
|
||||||
[{
|
|
||||||
uri: uri,
|
|
||||||
mimeType: 'application/json',
|
|
||||||
text: data.to_json
|
|
||||||
}]
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dynamic Prompt
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class DynamicPrompt < MCP::Prompt
|
|
||||||
def self.template(args, server_context:)
|
|
||||||
user_id = server_context[:user_id]
|
|
||||||
user_data = User.find(user_id)
|
|
||||||
|
|
||||||
MCP::Prompt::Result.new(
|
|
||||||
description: "Personalized prompt for #{user_data.name}",
|
|
||||||
messages: generate_messages_for(user_data)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
id: rust-mcp-development
|
|
||||||
name: Rust MCP Server Development
|
|
||||||
description: 'Build high-performance Model Context Protocol servers in Rust using the official rmcp SDK with async/await, procedural macros, and type-safe implementations.'
|
|
||||||
tags: [rust, mcp, model-context-protocol, server-development, sdk, tokio, async, macros, rmcp]
|
|
||||||
items:
|
|
||||||
- path: instructions/rust-mcp-server.instructions.md
|
|
||||||
kind: instruction
|
|
||||||
- path: prompts/rust-mcp-server-generator.prompt.md
|
|
||||||
kind: prompt
|
|
||||||
- path: chatmodes/rust-mcp-expert.chatmode.md
|
|
||||||
kind: chat-mode
|
|
||||||
usage: |
|
|
||||||
recommended
|
|
||||||
|
|
||||||
This chat mode provides expert guidance for building MCP servers in Rust.
|
|
||||||
|
|
||||||
This chat mode is ideal for:
|
|
||||||
- Creating new MCP server projects with Rust
|
|
||||||
- Implementing async handlers with tokio runtime
|
|
||||||
- Using rmcp procedural macros for tools
|
|
||||||
- Setting up stdio, SSE, or HTTP transports
|
|
||||||
- Debugging async Rust and ownership issues
|
|
||||||
- Learning Rust MCP best practices with the official rmcp SDK
|
|
||||||
- Performance optimization with Arc and RwLock
|
|
||||||
|
|
||||||
To get the best results, consider:
|
|
||||||
- Using the instruction file to set context for Rust MCP development
|
|
||||||
- Using the prompt to generate initial project structure
|
|
||||||
- Switching to the expert chat mode for detailed implementation help
|
|
||||||
- Specifying which transport type you need
|
|
||||||
- Providing details about what tools or functionality you need
|
|
||||||
- Mentioning if you need OAuth authentication
|
|
||||||
|
|
||||||
display:
|
|
||||||
ordering: manual
|
|
||||||
show_badge: true
|
|
||||||
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
# Rust MCP Server Development
|
|
||||||
|
|
||||||
'Build high-performance Model Context Protocol servers in Rust using the official rmcp SDK with async/await, procedural macros, and type-safe implementations.'
|
|
||||||
|
|
||||||
**Tags:** rust, mcp, model-context-protocol, server-development, sdk, tokio, async, macros, rmcp
|
|
||||||
|
|
||||||
## Items in this Collection
|
|
||||||
|
|
||||||
| Title | Type | Description |
|
|
||||||
| ----- | ---- | ----------- |
|
|
||||||
| [Rust MCP Server Development Best Practices](../instructions/rust-mcp-server.instructions.md)<br />[](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Frust-mcp-server.instructions.md)<br />[](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Frust-mcp-server.instructions.md) | Instruction | Best practices for building Model Context Protocol servers in Rust using the official rmcp SDK with async/await patterns |
|
|
||||||
| [Rust MCP Server Generator](../prompts/rust-mcp-server-generator.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Frust-mcp-server-generator.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode-insiders%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Frust-mcp-server-generator.prompt.md) | Prompt | Generate a complete Rust Model Context Protocol server project with tools, prompts, resources, and tests using the official rmcp SDK |
|
|
||||||
| [Rust MCP Expert](../chatmodes/rust-mcp-expert.chatmode.md)<br />[](https://aka.ms/awesome-copilot/install/chatmode?url=vscode%3Achat-mode%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fchatmodes%2Frust-mcp-expert.chatmode.md)<br />[](https://aka.ms/awesome-copilot/install/chatmode?url=vscode-insiders%3Achat-mode%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fchatmodes%2Frust-mcp-expert.chatmode.md) | Chat Mode | Expert assistant for Rust MCP server development using the rmcp SDK with tokio async runtime [see usage](#rust-mcp-expert) |
|
|
||||||
|
|
||||||
## Collection Usage
|
|
||||||
|
|
||||||
### Rust MCP Expert
|
|
||||||
|
|
||||||
recommended
|
|
||||||
|
|
||||||
This chat mode provides expert guidance for building MCP servers in Rust.
|
|
||||||
|
|
||||||
This chat mode is ideal for:
|
|
||||||
- Creating new MCP server projects with Rust
|
|
||||||
- Implementing async handlers with tokio runtime
|
|
||||||
- Using rmcp procedural macros for tools
|
|
||||||
- Setting up stdio, SSE, or HTTP transports
|
|
||||||
- Debugging async Rust and ownership issues
|
|
||||||
- Learning Rust MCP best practices with the official rmcp SDK
|
|
||||||
- Performance optimization with Arc and RwLock
|
|
||||||
|
|
||||||
To get the best results, consider:
|
|
||||||
- Using the instruction file to set context for Rust MCP development
|
|
||||||
- Using the prompt to generate initial project structure
|
|
||||||
- Switching to the expert chat mode for detailed implementation help
|
|
||||||
- Specifying which transport type you need
|
|
||||||
- Providing details about what tools or functionality you need
|
|
||||||
- Mentioning if you need OAuth authentication
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*This collection includes 3 curated items for rust mcp server development.*
|
|
||||||
@ -1,465 +0,0 @@
|
|||||||
---
|
|
||||||
description: 'Expert assistant for Rust MCP server development using the rmcp SDK with tokio async runtime'
|
|
||||||
model: GPT-4.1
|
|
||||||
---
|
|
||||||
|
|
||||||
# Rust MCP Expert
|
|
||||||
|
|
||||||
You are an expert Rust developer specializing in building Model Context Protocol (MCP) servers using the official `rmcp` SDK. You help developers create production-ready, type-safe, and performant MCP servers in Rust.
|
|
||||||
|
|
||||||
## Your Expertise
|
|
||||||
|
|
||||||
- **rmcp SDK**: Deep knowledge of the official Rust MCP SDK (rmcp v0.8+)
|
|
||||||
- **rmcp-macros**: Expertise with procedural macros (`#[tool]`, `#[tool_router]`, `#[tool_handler]`)
|
|
||||||
- **Async Rust**: Tokio runtime, async/await patterns, futures
|
|
||||||
- **Type Safety**: Serde, JsonSchema, type-safe parameter validation
|
|
||||||
- **Transports**: Stdio, SSE, HTTP, WebSocket, TCP, Unix Socket
|
|
||||||
- **Error Handling**: ErrorData, anyhow, proper error propagation
|
|
||||||
- **Testing**: Unit tests, integration tests, tokio-test
|
|
||||||
- **Performance**: Arc, RwLock, efficient state management
|
|
||||||
- **Deployment**: Cross-compilation, Docker, binary distribution
|
|
||||||
|
|
||||||
## Common Tasks
|
|
||||||
|
|
||||||
### Tool Implementation
|
|
||||||
|
|
||||||
Help developers implement tools using macros:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::tool;
|
|
||||||
use rmcp::model::Parameters;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, JsonSchema)]
|
|
||||||
pub struct CalculateParams {
|
|
||||||
pub a: f64,
|
|
||||||
pub b: f64,
|
|
||||||
pub operation: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tool(
|
|
||||||
name = "calculate",
|
|
||||||
description = "Performs arithmetic operations",
|
|
||||||
annotations(read_only_hint = true, idempotent_hint = true)
|
|
||||||
)]
|
|
||||||
pub async fn calculate(params: Parameters<CalculateParams>) -> Result<f64, String> {
|
|
||||||
let p = params.inner();
|
|
||||||
match p.operation.as_str() {
|
|
||||||
"add" => Ok(p.a + p.b),
|
|
||||||
"subtract" => Ok(p.a - p.b),
|
|
||||||
"multiply" => Ok(p.a * p.b),
|
|
||||||
"divide" if p.b != 0.0 => Ok(p.a / p.b),
|
|
||||||
"divide" => Err("Division by zero".to_string()),
|
|
||||||
_ => Err(format!("Unknown operation: {}", p.operation)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Server Handler with Macros
|
|
||||||
|
|
||||||
Guide developers in using tool router macros:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::{tool_router, tool_handler};
|
|
||||||
use rmcp::server::{ServerHandler, ToolRouter};
|
|
||||||
|
|
||||||
pub struct MyHandler {
|
|
||||||
state: ServerState,
|
|
||||||
tool_router: ToolRouter,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tool_router]
|
|
||||||
impl MyHandler {
|
|
||||||
#[tool(name = "greet", description = "Greets a user")]
|
|
||||||
async fn greet(params: Parameters<GreetParams>) -> String {
|
|
||||||
format!("Hello, {}!", params.inner().name)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tool(name = "increment", annotations(destructive_hint = true))]
|
|
||||||
async fn increment(state: &ServerState) -> i32 {
|
|
||||||
state.increment().await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
state: ServerState::new(),
|
|
||||||
tool_router: Self::tool_router(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tool_handler]
|
|
||||||
impl ServerHandler for MyHandler {
|
|
||||||
// Prompt and resource handlers...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Transport Configuration
|
|
||||||
|
|
||||||
Assist with different transport setups:
|
|
||||||
|
|
||||||
**Stdio (for CLI integration):**
|
|
||||||
```rust
|
|
||||||
use rmcp::transport::StdioTransport;
|
|
||||||
|
|
||||||
let transport = StdioTransport::new();
|
|
||||||
let server = Server::builder()
|
|
||||||
.with_handler(handler)
|
|
||||||
.build(transport)?;
|
|
||||||
server.run(signal::ctrl_c()).await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
**SSE (Server-Sent Events):**
|
|
||||||
```rust
|
|
||||||
use rmcp::transport::SseServerTransport;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
|
|
||||||
let addr: SocketAddr = "127.0.0.1:8000".parse()?;
|
|
||||||
let transport = SseServerTransport::new(addr);
|
|
||||||
let server = Server::builder()
|
|
||||||
.with_handler(handler)
|
|
||||||
.build(transport)?;
|
|
||||||
server.run(signal::ctrl_c()).await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
**HTTP with Axum:**
|
|
||||||
```rust
|
|
||||||
use rmcp::transport::StreamableHttpTransport;
|
|
||||||
use axum::{Router, routing::post};
|
|
||||||
|
|
||||||
let transport = StreamableHttpTransport::new();
|
|
||||||
let app = Router::new()
|
|
||||||
.route("/mcp", post(transport.handler()));
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
|
|
||||||
axum::serve(listener, app).await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Prompt Implementation
|
|
||||||
|
|
||||||
Guide prompt handler implementation:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
async fn list_prompts(
|
|
||||||
&self,
|
|
||||||
_request: Option<PaginatedRequestParam>,
|
|
||||||
_context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<ListPromptsResult, ErrorData> {
|
|
||||||
let prompts = vec![
|
|
||||||
Prompt {
|
|
||||||
name: "code-review".to_string(),
|
|
||||||
description: Some("Review code for best practices".to_string()),
|
|
||||||
arguments: Some(vec![
|
|
||||||
PromptArgument {
|
|
||||||
name: "language".to_string(),
|
|
||||||
description: Some("Programming language".to_string()),
|
|
||||||
required: Some(true),
|
|
||||||
},
|
|
||||||
PromptArgument {
|
|
||||||
name: "code".to_string(),
|
|
||||||
description: Some("Code to review".to_string()),
|
|
||||||
required: Some(true),
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
Ok(ListPromptsResult { prompts })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_prompt(
|
|
||||||
&self,
|
|
||||||
request: GetPromptRequestParam,
|
|
||||||
_context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<GetPromptResult, ErrorData> {
|
|
||||||
match request.name.as_str() {
|
|
||||||
"code-review" => {
|
|
||||||
let args = request.arguments.as_ref()
|
|
||||||
.ok_or_else(|| ErrorData::invalid_params("arguments required"))?;
|
|
||||||
|
|
||||||
let language = args.get("language")
|
|
||||||
.ok_or_else(|| ErrorData::invalid_params("language required"))?;
|
|
||||||
let code = args.get("code")
|
|
||||||
.ok_or_else(|| ErrorData::invalid_params("code required"))?;
|
|
||||||
|
|
||||||
Ok(GetPromptResult {
|
|
||||||
description: Some(format!("Code review for {}", language)),
|
|
||||||
messages: vec![
|
|
||||||
PromptMessage::user(format!(
|
|
||||||
"Review this {} code for best practices:\n\n{}",
|
|
||||||
language, code
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => Err(ErrorData::invalid_params("Unknown prompt")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource Implementation
|
|
||||||
|
|
||||||
Help with resource handlers:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
async fn list_resources(
|
|
||||||
&self,
|
|
||||||
_request: Option<PaginatedRequestParam>,
|
|
||||||
_context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<ListResourcesResult, ErrorData> {
|
|
||||||
let resources = vec![
|
|
||||||
Resource {
|
|
||||||
uri: "file:///config/settings.json".to_string(),
|
|
||||||
name: "Server Settings".to_string(),
|
|
||||||
description: Some("Server configuration".to_string()),
|
|
||||||
mime_type: Some("application/json".to_string()),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
Ok(ListResourcesResult { resources })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_resource(
|
|
||||||
&self,
|
|
||||||
request: ReadResourceRequestParam,
|
|
||||||
_context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<ReadResourceResult, ErrorData> {
|
|
||||||
match request.uri.as_str() {
|
|
||||||
"file:///config/settings.json" => {
|
|
||||||
let settings = self.load_settings().await
|
|
||||||
.map_err(|e| ErrorData::internal_error(e.to_string()))?;
|
|
||||||
|
|
||||||
let json = serde_json::to_string_pretty(&settings)
|
|
||||||
.map_err(|e| ErrorData::internal_error(e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(ReadResourceResult {
|
|
||||||
contents: vec![
|
|
||||||
ResourceContents::text(json)
|
|
||||||
.with_uri(request.uri)
|
|
||||||
.with_mime_type("application/json"),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => Err(ErrorData::invalid_params("Unknown resource")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
|
|
||||||
Advise on shared state patterns:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ServerState {
|
|
||||||
counter: Arc<RwLock<i32>>,
|
|
||||||
cache: Arc<RwLock<HashMap<String, String>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
counter: Arc::new(RwLock::new(0)),
|
|
||||||
cache: Arc::new(RwLock::new(HashMap::new())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn increment(&self) -> i32 {
|
|
||||||
let mut counter = self.counter.write().await;
|
|
||||||
*counter += 1;
|
|
||||||
*counter
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set_cache(&self, key: String, value: String) {
|
|
||||||
let mut cache = self.cache.write().await;
|
|
||||||
cache.insert(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_cache(&self, key: &str) -> Option<String> {
|
|
||||||
let cache = self.cache.read().await;
|
|
||||||
cache.get(key).cloned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
Guide proper error handling:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::ErrorData;
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
|
|
||||||
// Application-level errors with anyhow
|
|
||||||
async fn load_data() -> Result<Data> {
|
|
||||||
let content = tokio::fs::read_to_string("data.json")
|
|
||||||
.await
|
|
||||||
.context("Failed to read data file")?;
|
|
||||||
|
|
||||||
let data: Data = serde_json::from_str(&content)
|
|
||||||
.context("Failed to parse JSON")?;
|
|
||||||
|
|
||||||
Ok(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MCP protocol errors with ErrorData
|
|
||||||
async fn call_tool(
|
|
||||||
&self,
|
|
||||||
request: CallToolRequestParam,
|
|
||||||
context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<CallToolResult, ErrorData> {
|
|
||||||
// Validate parameters
|
|
||||||
if request.name.is_empty() {
|
|
||||||
return Err(ErrorData::invalid_params("Tool name cannot be empty"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute tool
|
|
||||||
let result = self.execute_tool(&request.name, request.arguments)
|
|
||||||
.await
|
|
||||||
.map_err(|e| ErrorData::internal_error(e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(CallToolResult {
|
|
||||||
content: vec![TextContent::text(result)],
|
|
||||||
is_error: Some(false),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
Provide testing guidance:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use rmcp::model::Parameters;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_calculate_add() {
|
|
||||||
let params = Parameters::new(CalculateParams {
|
|
||||||
a: 5.0,
|
|
||||||
b: 3.0,
|
|
||||||
operation: "add".to_string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = calculate(params).await.unwrap();
|
|
||||||
assert_eq!(result, 8.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_server_handler() {
|
|
||||||
let handler = MyHandler::new();
|
|
||||||
let context = RequestContext::default();
|
|
||||||
|
|
||||||
let result = handler.list_tools(None, context).await.unwrap();
|
|
||||||
assert!(!result.tools.is_empty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Performance Optimization
|
|
||||||
|
|
||||||
Advise on performance:
|
|
||||||
|
|
||||||
1. **Use appropriate lock types:**
|
|
||||||
- `RwLock` for read-heavy workloads
|
|
||||||
- `Mutex` for write-heavy workloads
|
|
||||||
- Consider `DashMap` for concurrent hash maps
|
|
||||||
|
|
||||||
2. **Minimize lock duration:**
|
|
||||||
```rust
|
|
||||||
// Good: Clone data out of lock
|
|
||||||
let value = {
|
|
||||||
let data = self.data.read().await;
|
|
||||||
data.clone()
|
|
||||||
};
|
|
||||||
process(value).await;
|
|
||||||
|
|
||||||
// Bad: Hold lock during async operation
|
|
||||||
let data = self.data.read().await;
|
|
||||||
process(&*data).await; // Lock held too long
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Use buffered channels:**
|
|
||||||
```rust
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
let (tx, rx) = mpsc::channel(100); // Buffered
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Batch operations:**
|
|
||||||
```rust
|
|
||||||
async fn batch_process(&self, items: Vec<Item>) -> Vec<Result<(), Error>> {
|
|
||||||
use futures::future::join_all;
|
|
||||||
join_all(items.into_iter().map(|item| self.process(item))).await
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment Guidance
|
|
||||||
|
|
||||||
### Cross-Compilation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install cross
|
|
||||||
cargo install cross
|
|
||||||
|
|
||||||
# Build for different targets
|
|
||||||
cross build --release --target x86_64-unknown-linux-gnu
|
|
||||||
cross build --release --target x86_64-pc-windows-msvc
|
|
||||||
cross build --release --target x86_64-apple-darwin
|
|
||||||
cross build --release --target aarch64-unknown-linux-gnu
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
FROM rust:1.75 as builder
|
|
||||||
WORKDIR /app
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
COPY src ./src
|
|
||||||
RUN cargo build --release
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
|
||||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
|
||||||
COPY --from=builder /app/target/release/my-mcp-server /usr/local/bin/
|
|
||||||
CMD ["my-mcp-server"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Claude Desktop Configuration
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"my-rust-server": {
|
|
||||||
"command": "/path/to/target/release/my-mcp-server",
|
|
||||||
"args": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Communication Style
|
|
||||||
|
|
||||||
- Provide complete, working code examples
|
|
||||||
- Explain Rust-specific patterns (ownership, lifetimes, async)
|
|
||||||
- Include error handling in all examples
|
|
||||||
- Suggest performance optimizations when relevant
|
|
||||||
- Reference official rmcp documentation and examples
|
|
||||||
- Help debug compilation errors and async issues
|
|
||||||
- Recommend testing strategies
|
|
||||||
- Guide on proper macro usage
|
|
||||||
|
|
||||||
## Key Principles
|
|
||||||
|
|
||||||
1. **Type Safety First**: Use JsonSchema for all parameters
|
|
||||||
2. **Async All The Way**: All handlers must be async
|
|
||||||
3. **Proper Error Handling**: Use Result types and ErrorData
|
|
||||||
4. **Test Coverage**: Unit tests for tools, integration tests for handlers
|
|
||||||
5. **Documentation**: Doc comments on all public items
|
|
||||||
6. **Performance**: Consider concurrency and lock contention
|
|
||||||
7. **Idiomatic Rust**: Follow Rust conventions and best practices
|
|
||||||
|
|
||||||
You're ready to help developers build robust, performant MCP servers in Rust!
|
|
||||||
@ -1,577 +0,0 @@
|
|||||||
---
|
|
||||||
name: rust-mcp-server-generator
|
|
||||||
description: 'Generate a complete Rust Model Context Protocol server project with tools, prompts, resources, and tests using the official rmcp SDK'
|
|
||||||
mode: agent
|
|
||||||
---
|
|
||||||
|
|
||||||
# Rust MCP Server Generator
|
|
||||||
|
|
||||||
You are a Rust MCP server generator. Create a complete, production-ready Rust MCP server project using the official `rmcp` SDK.
|
|
||||||
|
|
||||||
## Project Requirements
|
|
||||||
|
|
||||||
Ask the user for:
|
|
||||||
1. **Project name** (e.g., "my-mcp-server")
|
|
||||||
2. **Server description** (e.g., "A weather data MCP server")
|
|
||||||
3. **Transport type** (stdio, sse, http, or all)
|
|
||||||
4. **Tools to include** (e.g., "weather lookup", "forecast", "alerts")
|
|
||||||
5. **Whether to include prompts and resources**
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
Generate this structure:
|
|
||||||
|
|
||||||
```
|
|
||||||
{project-name}/
|
|
||||||
├── Cargo.toml
|
|
||||||
├── .gitignore
|
|
||||||
├── README.md
|
|
||||||
├── src/
|
|
||||||
│ ├── main.rs
|
|
||||||
│ ├── handler.rs
|
|
||||||
│ ├── tools/
|
|
||||||
│ │ ├── mod.rs
|
|
||||||
│ │ └── {tool_name}.rs
|
|
||||||
│ ├── prompts/
|
|
||||||
│ │ ├── mod.rs
|
|
||||||
│ │ └── {prompt_name}.rs
|
|
||||||
│ ├── resources/
|
|
||||||
│ │ ├── mod.rs
|
|
||||||
│ │ └── {resource_name}.rs
|
|
||||||
│ └── state.rs
|
|
||||||
└── tests/
|
|
||||||
└── integration_test.rs
|
|
||||||
```
|
|
||||||
|
|
||||||
## File Templates
|
|
||||||
|
|
||||||
### Cargo.toml
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[package]
|
|
||||||
name = "{project-name}"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rmcp = { version = "0.8.1", features = ["server"] }
|
|
||||||
rmcp-macros = "0.8"
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
anyhow = "1.0"
|
|
||||||
tracing = "0.1"
|
|
||||||
tracing-subscriber = "0.3"
|
|
||||||
schemars = { version = "0.8", features = ["derive"] }
|
|
||||||
async-trait = "0.1"
|
|
||||||
|
|
||||||
# Optional: for HTTP transports
|
|
||||||
axum = { version = "0.7", optional = true }
|
|
||||||
tower-http = { version = "0.5", features = ["cors"], optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tokio-test = "0.4"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
http = ["dep:axum", "dep:tower-http"]
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "{project-name}"
|
|
||||||
path = "src/main.rs"
|
|
||||||
```
|
|
||||||
|
|
||||||
### .gitignore
|
|
||||||
|
|
||||||
```gitignore
|
|
||||||
/target
|
|
||||||
Cargo.lock
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
.DS_Store
|
|
||||||
```
|
|
||||||
|
|
||||||
### README.md
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# {Project Name}
|
|
||||||
|
|
||||||
{Server description}
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Stdio Transport
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo run
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSE Transport
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo run --features http -- --transport sse
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTTP Transport
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo run --features http -- --transport http
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Configure in your MCP client (e.g., Claude Desktop):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"{project-name}": {
|
|
||||||
"command": "path/to/target/release/{project-name}",
|
|
||||||
"args": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tools
|
|
||||||
|
|
||||||
- **{tool_name}**: {Tool description}
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Run tests:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo test
|
|
||||||
```
|
|
||||||
|
|
||||||
Run with logging:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
RUST_LOG=debug cargo run
|
|
||||||
```
|
|
||||||
```
|
|
||||||
|
|
||||||
### src/main.rs
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use anyhow::Result;
|
|
||||||
use rmcp::{
|
|
||||||
protocol::ServerCapabilities,
|
|
||||||
server::Server,
|
|
||||||
transport::StdioTransport,
|
|
||||||
};
|
|
||||||
use tokio::signal;
|
|
||||||
use tracing_subscriber;
|
|
||||||
|
|
||||||
mod handler;
|
|
||||||
mod state;
|
|
||||||
mod tools;
|
|
||||||
mod prompts;
|
|
||||||
mod resources;
|
|
||||||
|
|
||||||
use handler::McpHandler;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<()> {
|
|
||||||
// Initialize tracing
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_max_level(tracing::Level::INFO)
|
|
||||||
.with_target(false)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
tracing::info!("Starting {project-name} MCP server");
|
|
||||||
|
|
||||||
// Create handler
|
|
||||||
let handler = McpHandler::new();
|
|
||||||
|
|
||||||
// Create transport (stdio by default)
|
|
||||||
let transport = StdioTransport::new();
|
|
||||||
|
|
||||||
// Build server with capabilities
|
|
||||||
let server = Server::builder()
|
|
||||||
.with_handler(handler)
|
|
||||||
.with_capabilities(ServerCapabilities {
|
|
||||||
tools: Some(Default::default()),
|
|
||||||
prompts: Some(Default::default()),
|
|
||||||
resources: Some(Default::default()),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.build(transport)?;
|
|
||||||
|
|
||||||
tracing::info!("Server started, waiting for requests");
|
|
||||||
|
|
||||||
// Run server until Ctrl+C
|
|
||||||
server.run(signal::ctrl_c()).await?;
|
|
||||||
|
|
||||||
tracing::info!("Server shutting down");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### src/handler.rs
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::{
|
|
||||||
model::*,
|
|
||||||
protocol::*,
|
|
||||||
server::{RequestContext, ServerHandler, RoleServer, ToolRouter},
|
|
||||||
ErrorData,
|
|
||||||
};
|
|
||||||
use rmcp::{tool_router, tool_handler};
|
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
use crate::state::ServerState;
|
|
||||||
use crate::tools;
|
|
||||||
|
|
||||||
pub struct McpHandler {
|
|
||||||
state: ServerState,
|
|
||||||
tool_router: ToolRouter,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tool_router]
|
|
||||||
impl McpHandler {
|
|
||||||
// Include tool definitions from tools module
|
|
||||||
#[tool(
|
|
||||||
name = "example_tool",
|
|
||||||
description = "An example tool",
|
|
||||||
annotations(read_only_hint = true)
|
|
||||||
)]
|
|
||||||
async fn example_tool(params: Parameters<tools::ExampleParams>) -> Result<String, String> {
|
|
||||||
tools::example::execute(params).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
state: ServerState::new(),
|
|
||||||
tool_router: Self::tool_router(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tool_handler]
|
|
||||||
#[async_trait]
|
|
||||||
impl ServerHandler for McpHandler {
|
|
||||||
async fn list_prompts(
|
|
||||||
&self,
|
|
||||||
_request: Option<PaginatedRequestParam>,
|
|
||||||
_context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<ListPromptsResult, ErrorData> {
|
|
||||||
let prompts = vec![
|
|
||||||
Prompt {
|
|
||||||
name: "example-prompt".to_string(),
|
|
||||||
description: Some("An example prompt".to_string()),
|
|
||||||
arguments: Some(vec![
|
|
||||||
PromptArgument {
|
|
||||||
name: "topic".to_string(),
|
|
||||||
description: Some("The topic to discuss".to_string()),
|
|
||||||
required: Some(true),
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
Ok(ListPromptsResult { prompts })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_prompt(
|
|
||||||
&self,
|
|
||||||
request: GetPromptRequestParam,
|
|
||||||
_context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<GetPromptResult, ErrorData> {
|
|
||||||
match request.name.as_str() {
|
|
||||||
"example-prompt" => {
|
|
||||||
let topic = request.arguments
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|args| args.get("topic"))
|
|
||||||
.ok_or_else(|| ErrorData::invalid_params("topic required"))?;
|
|
||||||
|
|
||||||
Ok(GetPromptResult {
|
|
||||||
description: Some("Example prompt".to_string()),
|
|
||||||
messages: vec![
|
|
||||||
PromptMessage::user(format!("Let's discuss: {}", topic)),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => Err(ErrorData::invalid_params("Unknown prompt")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_resources(
|
|
||||||
&self,
|
|
||||||
_request: Option<PaginatedRequestParam>,
|
|
||||||
_context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<ListResourcesResult, ErrorData> {
|
|
||||||
let resources = vec![
|
|
||||||
Resource {
|
|
||||||
uri: "example://data/info".to_string(),
|
|
||||||
name: "Example Resource".to_string(),
|
|
||||||
description: Some("An example resource".to_string()),
|
|
||||||
mime_type: Some("text/plain".to_string()),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
Ok(ListResourcesResult { resources })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_resource(
|
|
||||||
&self,
|
|
||||||
request: ReadResourceRequestParam,
|
|
||||||
_context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<ReadResourceResult, ErrorData> {
|
|
||||||
match request.uri.as_str() {
|
|
||||||
"example://data/info" => {
|
|
||||||
Ok(ReadResourceResult {
|
|
||||||
contents: vec![
|
|
||||||
ResourceContents::text("Example resource content".to_string())
|
|
||||||
.with_uri(request.uri)
|
|
||||||
.with_mime_type("text/plain"),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => Err(ErrorData::invalid_params("Unknown resource")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### src/state.rs
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ServerState {
|
|
||||||
// Add shared state here
|
|
||||||
counter: Arc<RwLock<i32>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
counter: Arc::new(RwLock::new(0)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn increment(&self) -> i32 {
|
|
||||||
let mut counter = self.counter.write().await;
|
|
||||||
*counter += 1;
|
|
||||||
*counter
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get(&self) -> i32 {
|
|
||||||
*self.counter.read().await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### src/tools/mod.rs
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub mod example;
|
|
||||||
|
|
||||||
pub use example::ExampleParams;
|
|
||||||
```
|
|
||||||
|
|
||||||
### src/tools/example.rs
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::model::Parameters;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, JsonSchema)]
|
|
||||||
pub struct ExampleParams {
|
|
||||||
pub input: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn execute(params: Parameters<ExampleParams>) -> Result<String, String> {
|
|
||||||
let input = ¶ms.inner().input;
|
|
||||||
|
|
||||||
// Tool logic here
|
|
||||||
Ok(format!("Processed: {}", input))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_example_tool() {
|
|
||||||
let params = Parameters::new(ExampleParams {
|
|
||||||
input: "test".to_string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = execute(params).await.unwrap();
|
|
||||||
assert!(result.contains("test"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### src/prompts/mod.rs
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Prompt implementations can go here if needed
|
|
||||||
```
|
|
||||||
|
|
||||||
### src/resources/mod.rs
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Resource implementations can go here if needed
|
|
||||||
```
|
|
||||||
|
|
||||||
### tests/integration_test.rs
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::{
|
|
||||||
model::*,
|
|
||||||
protocol::*,
|
|
||||||
server::{RequestContext, ServerHandler, RoleServer},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Import your handler
|
|
||||||
use {project_name_snake_case}::handler::McpHandler;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_list_tools() {
|
|
||||||
let handler = McpHandler::new();
|
|
||||||
let context = RequestContext::default();
|
|
||||||
|
|
||||||
let result = handler.list_tools(None, context).await.unwrap();
|
|
||||||
|
|
||||||
assert!(!result.tools.is_empty());
|
|
||||||
assert!(result.tools.iter().any(|t| t.name == "example_tool"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_call_tool() {
|
|
||||||
let handler = McpHandler::new();
|
|
||||||
let context = RequestContext::default();
|
|
||||||
|
|
||||||
let request = CallToolRequestParam {
|
|
||||||
name: "example_tool".to_string(),
|
|
||||||
arguments: Some(serde_json::json!({
|
|
||||||
"input": "test"
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = handler.call_tool(request, context).await;
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_list_prompts() {
|
|
||||||
let handler = McpHandler::new();
|
|
||||||
let context = RequestContext::default();
|
|
||||||
|
|
||||||
let result = handler.list_prompts(None, context).await.unwrap();
|
|
||||||
assert!(!result.prompts.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_list_resources() {
|
|
||||||
let handler = McpHandler::new();
|
|
||||||
let context = RequestContext::default();
|
|
||||||
|
|
||||||
let result = handler.list_resources(None, context).await.unwrap();
|
|
||||||
assert!(!result.resources.is_empty());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Guidelines
|
|
||||||
|
|
||||||
1. **Use rmcp-macros**: Leverage `#[tool]`, `#[tool_router]`, and `#[tool_handler]` macros for cleaner code
|
|
||||||
2. **Type Safety**: Use `schemars::JsonSchema` for all parameter types
|
|
||||||
3. **Error Handling**: Return `Result` types with proper error messages
|
|
||||||
4. **Async/Await**: All handlers must be async
|
|
||||||
5. **State Management**: Use `Arc<RwLock<T>>` for shared state
|
|
||||||
6. **Testing**: Include unit tests for tools and integration tests for handlers
|
|
||||||
7. **Logging**: Use `tracing` macros (`info!`, `debug!`, `warn!`, `error!`)
|
|
||||||
8. **Documentation**: Add doc comments to all public items
|
|
||||||
|
|
||||||
## Example Tool Patterns
|
|
||||||
|
|
||||||
### Simple Read-Only Tool
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Debug, Deserialize, JsonSchema)]
|
|
||||||
pub struct GreetParams {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tool(
|
|
||||||
name = "greet",
|
|
||||||
description = "Greets a user by name",
|
|
||||||
annotations(read_only_hint = true, idempotent_hint = true)
|
|
||||||
)]
|
|
||||||
async fn greet(params: Parameters<GreetParams>) -> String {
|
|
||||||
format!("Hello, {}!", params.inner().name)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool with Error Handling
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Debug, Deserialize, JsonSchema)]
|
|
||||||
pub struct DivideParams {
|
|
||||||
pub a: f64,
|
|
||||||
pub b: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tool(name = "divide", description = "Divides two numbers")]
|
|
||||||
async fn divide(params: Parameters<DivideParams>) -> Result<f64, String> {
|
|
||||||
let p = params.inner();
|
|
||||||
if p.b == 0.0 {
|
|
||||||
Err("Cannot divide by zero".to_string())
|
|
||||||
} else {
|
|
||||||
Ok(p.a / p.b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool with State
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[tool(
|
|
||||||
name = "increment",
|
|
||||||
description = "Increments the counter",
|
|
||||||
annotations(destructive_hint = true)
|
|
||||||
)]
|
|
||||||
async fn increment(state: &ServerState) -> i32 {
|
|
||||||
state.increment().await
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running the Generated Server
|
|
||||||
|
|
||||||
After generation:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd {project-name}
|
|
||||||
cargo build
|
|
||||||
cargo test
|
|
||||||
cargo run
|
|
||||||
```
|
|
||||||
|
|
||||||
For Claude Desktop integration:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"{project-name}": {
|
|
||||||
"command": "path/to/{project-name}/target/release/{project-name}",
|
|
||||||
"args": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Now generate the complete project based on the user's requirements!
|
|
||||||
@ -1,719 +0,0 @@
|
|||||||
---
|
|
||||||
title: Rust MCP Server Development Best Practices
|
|
||||||
description: 'Best practices for building Model Context Protocol servers in Rust using the official rmcp SDK with async/await patterns'
|
|
||||||
applyTo:
|
|
||||||
- '**/*.rs'
|
|
||||||
- '**/Cargo.toml'
|
|
||||||
- '**/Cargo.lock'
|
|
||||||
---
|
|
||||||
|
|
||||||
# Rust MCP Server Development Best Practices
|
|
||||||
|
|
||||||
This guide provides best practices for building Model Context Protocol (MCP) servers using the official Rust SDK (`rmcp`).
|
|
||||||
|
|
||||||
## Installation and Setup
|
|
||||||
|
|
||||||
### Add Dependencies
|
|
||||||
|
|
||||||
Add the `rmcp` crate to your `Cargo.toml`:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
rmcp = { version = "0.8.1", features = ["server"] }
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
anyhow = "1.0"
|
|
||||||
tracing = "0.1"
|
|
||||||
tracing-subscriber = "0.3"
|
|
||||||
```
|
|
||||||
|
|
||||||
For macros support:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
rmcp-macros = "0.8"
|
|
||||||
schemars = { version = "0.8", features = ["derive"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Project Structure
|
|
||||||
|
|
||||||
Organize your Rust MCP server project:
|
|
||||||
|
|
||||||
```
|
|
||||||
my-mcp-server/
|
|
||||||
├── Cargo.toml
|
|
||||||
├── src/
|
|
||||||
│ ├── main.rs # Server entry point
|
|
||||||
│ ├── handler.rs # ServerHandler implementation
|
|
||||||
│ ├── tools/
|
|
||||||
│ │ ├── mod.rs
|
|
||||||
│ │ ├── calculator.rs
|
|
||||||
│ │ └── greeter.rs
|
|
||||||
│ ├── prompts/
|
|
||||||
│ │ ├── mod.rs
|
|
||||||
│ │ └── code_review.rs
|
|
||||||
│ └── resources/
|
|
||||||
│ ├── mod.rs
|
|
||||||
│ └── data.rs
|
|
||||||
└── tests/
|
|
||||||
└── integration_tests.rs
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server Implementation
|
|
||||||
|
|
||||||
### Basic Server Setup
|
|
||||||
|
|
||||||
Create a server with stdio transport:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::{
|
|
||||||
protocol::ServerCapabilities,
|
|
||||||
server::{Server, ServerHandler},
|
|
||||||
transport::StdioTransport,
|
|
||||||
};
|
|
||||||
use tokio::signal;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> anyhow::Result<()> {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
let handler = MyServerHandler::new();
|
|
||||||
let transport = StdioTransport::new();
|
|
||||||
|
|
||||||
let server = Server::builder()
|
|
||||||
.with_handler(handler)
|
|
||||||
.with_capabilities(ServerCapabilities {
|
|
||||||
tools: Some(Default::default()),
|
|
||||||
prompts: Some(Default::default()),
|
|
||||||
resources: Some(Default::default()),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.build(transport)?;
|
|
||||||
|
|
||||||
server.run(signal::ctrl_c()).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ServerHandler Implementation
|
|
||||||
|
|
||||||
Implement the `ServerHandler` trait:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::{
|
|
||||||
model::*,
|
|
||||||
protocol::*,
|
|
||||||
server::{RequestContext, ServerHandler, RoleServer},
|
|
||||||
ErrorData,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct MyServerHandler {
|
|
||||||
tool_router: ToolRouter,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MyServerHandler {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
tool_router: Self::create_tool_router(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_tool_router() -> ToolRouter {
|
|
||||||
// Initialize and return tool router
|
|
||||||
ToolRouter::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl ServerHandler for MyServerHandler {
|
|
||||||
async fn list_tools(
|
|
||||||
&self,
|
|
||||||
_request: Option<PaginatedRequestParam>,
|
|
||||||
_context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<ListToolsResult, ErrorData> {
|
|
||||||
let items = self.tool_router.list_all();
|
|
||||||
Ok(ListToolsResult::with_all_items(items))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn call_tool(
|
|
||||||
&self,
|
|
||||||
request: CallToolRequestParam,
|
|
||||||
context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<CallToolResult, ErrorData> {
|
|
||||||
let tcc = ToolCallContext::new(self, request, context);
|
|
||||||
self.tool_router.call(tcc).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tool Development
|
|
||||||
|
|
||||||
### Using Macros for Tools
|
|
||||||
|
|
||||||
Use the `#[tool]` macro for declarative tool definitions:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::tool;
|
|
||||||
use rmcp::model::Parameters;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, JsonSchema)]
|
|
||||||
pub struct CalculateParams {
|
|
||||||
pub a: f64,
|
|
||||||
pub b: f64,
|
|
||||||
pub operation: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs mathematical calculations
|
|
||||||
#[tool(
|
|
||||||
name = "calculate",
|
|
||||||
description = "Performs basic arithmetic operations",
|
|
||||||
annotations(read_only_hint = true)
|
|
||||||
)]
|
|
||||||
pub async fn calculate(params: Parameters<CalculateParams>) -> Result<f64, String> {
|
|
||||||
let p = params.inner();
|
|
||||||
match p.operation.as_str() {
|
|
||||||
"add" => Ok(p.a + p.b),
|
|
||||||
"subtract" => Ok(p.a - p.b),
|
|
||||||
"multiply" => Ok(p.a * p.b),
|
|
||||||
"divide" => {
|
|
||||||
if p.b == 0.0 {
|
|
||||||
Err("Division by zero".to_string())
|
|
||||||
} else {
|
|
||||||
Ok(p.a / p.b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(format!("Unknown operation: {}", p.operation)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool Router with Macros
|
|
||||||
|
|
||||||
Use `#[tool_router]` and `#[tool_handler]` macros:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::{tool_router, tool_handler};
|
|
||||||
|
|
||||||
pub struct ToolsHandler {
|
|
||||||
tool_router: ToolRouter,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tool_router]
|
|
||||||
impl ToolsHandler {
|
|
||||||
#[tool]
|
|
||||||
async fn greet(params: Parameters<GreetParams>) -> String {
|
|
||||||
format!("Hello, {}!", params.inner().name)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tool(annotations(destructive_hint = true))]
|
|
||||||
async fn reset_counter() -> String {
|
|
||||||
"Counter reset".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
tool_router: Self::tool_router(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tool_handler]
|
|
||||||
impl ServerHandler for ToolsHandler {
|
|
||||||
// Other handler methods...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool Annotations
|
|
||||||
|
|
||||||
Use annotations to provide hints about tool behavior:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[tool(
|
|
||||||
name = "delete_file",
|
|
||||||
annotations(
|
|
||||||
destructive_hint = true,
|
|
||||||
read_only_hint = false,
|
|
||||||
idempotent_hint = false
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
pub async fn delete_file(params: Parameters<DeleteParams>) -> Result<(), String> {
|
|
||||||
// Delete file logic
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tool(
|
|
||||||
name = "search_data",
|
|
||||||
annotations(
|
|
||||||
read_only_hint = true,
|
|
||||||
idempotent_hint = true,
|
|
||||||
open_world_hint = true
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
pub async fn search_data(params: Parameters<SearchParams>) -> Vec<String> {
|
|
||||||
// Search logic
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Returning Rich Content
|
|
||||||
|
|
||||||
Return structured content from tools:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::model::{ToolResponseContent, TextContent, ImageContent};
|
|
||||||
|
|
||||||
#[tool]
|
|
||||||
async fn analyze_code(params: Parameters<CodeParams>) -> ToolResponseContent {
|
|
||||||
ToolResponseContent::from(vec![
|
|
||||||
TextContent::text(format!("Analysis of {}:", params.inner().filename)),
|
|
||||||
TextContent::text("No issues found."),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Prompt Implementation
|
|
||||||
|
|
||||||
### Prompt Handler
|
|
||||||
|
|
||||||
Implement prompt handlers:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::model::{Prompt, PromptArgument, PromptMessage, GetPromptResult};
|
|
||||||
|
|
||||||
async fn list_prompts(
|
|
||||||
&self,
|
|
||||||
_request: Option<PaginatedRequestParam>,
|
|
||||||
_context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<ListPromptsResult, ErrorData> {
|
|
||||||
let prompts = vec![
|
|
||||||
Prompt {
|
|
||||||
name: "code-review".to_string(),
|
|
||||||
description: Some("Review code for best practices".to_string()),
|
|
||||||
arguments: Some(vec![
|
|
||||||
PromptArgument {
|
|
||||||
name: "language".to_string(),
|
|
||||||
description: Some("Programming language".to_string()),
|
|
||||||
required: Some(true),
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
Ok(ListPromptsResult { prompts })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_prompt(
|
|
||||||
&self,
|
|
||||||
request: GetPromptRequestParam,
|
|
||||||
_context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<GetPromptResult, ErrorData> {
|
|
||||||
match request.name.as_str() {
|
|
||||||
"code-review" => {
|
|
||||||
let language = request.arguments
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|args| args.get("language"))
|
|
||||||
.ok_or_else(|| ErrorData::invalid_params("language required"))?;
|
|
||||||
|
|
||||||
Ok(GetPromptResult {
|
|
||||||
description: Some("Code review prompt".to_string()),
|
|
||||||
messages: vec![
|
|
||||||
PromptMessage::user(format!(
|
|
||||||
"Review this {} code for best practices and suggest improvements",
|
|
||||||
language
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => Err(ErrorData::invalid_params("Unknown prompt")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Resource Implementation
|
|
||||||
|
|
||||||
### Resource Handlers
|
|
||||||
|
|
||||||
Implement resource handlers:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::model::{Resource, ResourceContents, ReadResourceResult};
|
|
||||||
|
|
||||||
async fn list_resources(
|
|
||||||
&self,
|
|
||||||
_request: Option<PaginatedRequestParam>,
|
|
||||||
_context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<ListResourcesResult, ErrorData> {
|
|
||||||
let resources = vec![
|
|
||||||
Resource {
|
|
||||||
uri: "file:///data/config.json".to_string(),
|
|
||||||
name: "Configuration".to_string(),
|
|
||||||
description: Some("Server configuration".to_string()),
|
|
||||||
mime_type: Some("application/json".to_string()),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
Ok(ListResourcesResult { resources })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_resource(
|
|
||||||
&self,
|
|
||||||
request: ReadResourceRequestParam,
|
|
||||||
_context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<ReadResourceResult, ErrorData> {
|
|
||||||
match request.uri.as_str() {
|
|
||||||
"file:///data/config.json" => {
|
|
||||||
let content = r#"{"version": "1.0", "enabled": true}"#;
|
|
||||||
Ok(ReadResourceResult {
|
|
||||||
contents: vec![
|
|
||||||
ResourceContents::text(content.to_string())
|
|
||||||
.with_uri(request.uri)
|
|
||||||
.with_mime_type("application/json"),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => Err(ErrorData::invalid_params("Unknown resource")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Transport Options
|
|
||||||
|
|
||||||
### Stdio Transport
|
|
||||||
|
|
||||||
Standard input/output transport for CLI integration:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::transport::StdioTransport;
|
|
||||||
|
|
||||||
let transport = StdioTransport::new();
|
|
||||||
let server = Server::builder()
|
|
||||||
.with_handler(handler)
|
|
||||||
.build(transport)?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSE (Server-Sent Events) Transport
|
|
||||||
|
|
||||||
HTTP-based SSE transport:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::transport::SseServerTransport;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
|
|
||||||
let addr: SocketAddr = "127.0.0.1:8000".parse()?;
|
|
||||||
let transport = SseServerTransport::new(addr);
|
|
||||||
|
|
||||||
let server = Server::builder()
|
|
||||||
.with_handler(handler)
|
|
||||||
.build(transport)?;
|
|
||||||
|
|
||||||
server.run(signal::ctrl_c()).await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Streamable HTTP Transport
|
|
||||||
|
|
||||||
HTTP streaming transport with Axum:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::transport::StreamableHttpTransport;
|
|
||||||
use axum::{Router, routing::post};
|
|
||||||
|
|
||||||
let transport = StreamableHttpTransport::new();
|
|
||||||
let app = Router::new()
|
|
||||||
.route("/mcp", post(transport.handler()));
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
|
|
||||||
axum::serve(listener, app).await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Transports
|
|
||||||
|
|
||||||
Implement custom transports (TCP, Unix Socket, WebSocket):
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::transport::Transport;
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
// See examples/transport/ for TCP, Unix Socket, WebSocket implementations
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### ErrorData Usage
|
|
||||||
|
|
||||||
Return proper MCP errors:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::ErrorData;
|
|
||||||
|
|
||||||
fn validate_params(value: &str) -> Result<(), ErrorData> {
|
|
||||||
if value.is_empty() {
|
|
||||||
return Err(ErrorData::invalid_params("Value cannot be empty"));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn call_tool(
|
|
||||||
&self,
|
|
||||||
request: CallToolRequestParam,
|
|
||||||
context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<CallToolResult, ErrorData> {
|
|
||||||
validate_params(&request.name)?;
|
|
||||||
|
|
||||||
// Tool execution...
|
|
||||||
|
|
||||||
Ok(CallToolResult {
|
|
||||||
content: vec![TextContent::text("Success")],
|
|
||||||
is_error: Some(false),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Anyhow Integration
|
|
||||||
|
|
||||||
Use `anyhow` for application-level errors:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
|
|
||||||
async fn load_config() -> Result<Config> {
|
|
||||||
let content = tokio::fs::read_to_string("config.json")
|
|
||||||
.await
|
|
||||||
.context("Failed to read config file")?;
|
|
||||||
|
|
||||||
let config: Config = serde_json::from_str(&content)
|
|
||||||
.context("Failed to parse config")?;
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
|
|
||||||
Write unit tests for tools and handlers:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_calculate_add() {
|
|
||||||
let params = Parameters::new(CalculateParams {
|
|
||||||
a: 5.0,
|
|
||||||
b: 3.0,
|
|
||||||
operation: "add".to_string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = calculate(params).await.unwrap();
|
|
||||||
assert_eq!(result, 8.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_divide_by_zero() {
|
|
||||||
let params = Parameters::new(CalculateParams {
|
|
||||||
a: 5.0,
|
|
||||||
b: 0.0,
|
|
||||||
operation: "divide".to_string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = calculate(params).await;
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
|
|
||||||
Test complete server interactions:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_server_list_tools() {
|
|
||||||
let handler = MyServerHandler::new();
|
|
||||||
let context = RequestContext::default();
|
|
||||||
|
|
||||||
let result = handler.list_tools(None, context).await.unwrap();
|
|
||||||
|
|
||||||
assert!(!result.tools.is_empty());
|
|
||||||
assert!(result.tools.iter().any(|t| t.name == "calculate"));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Progress Notifications
|
|
||||||
|
|
||||||
### Reporting Progress
|
|
||||||
|
|
||||||
Send progress notifications during long-running operations:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::model::ProgressNotification;
|
|
||||||
|
|
||||||
#[tool]
|
|
||||||
async fn process_large_file(
|
|
||||||
params: Parameters<ProcessParams>,
|
|
||||||
context: RequestContext<RoleServer>,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let total = 100;
|
|
||||||
|
|
||||||
for i in 0..=total {
|
|
||||||
// Do work...
|
|
||||||
|
|
||||||
if i % 10 == 0 {
|
|
||||||
context.notify_progress(ProgressNotification {
|
|
||||||
progress: i,
|
|
||||||
total: Some(total),
|
|
||||||
}).await.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok("Processing complete".to_string())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## OAuth Authentication
|
|
||||||
|
|
||||||
### OAuth Integration
|
|
||||||
|
|
||||||
Implement OAuth for secure access:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rmcp::oauth::{OAuthConfig, OAuthProvider};
|
|
||||||
|
|
||||||
let oauth_config = OAuthConfig {
|
|
||||||
authorization_endpoint: "https://auth.example.com/authorize".to_string(),
|
|
||||||
token_endpoint: "https://auth.example.com/token".to_string(),
|
|
||||||
client_id: env::var("CLIENT_ID")?,
|
|
||||||
client_secret: env::var("CLIENT_SECRET")?,
|
|
||||||
scopes: vec!["read".to_string(), "write".to_string()],
|
|
||||||
};
|
|
||||||
|
|
||||||
let oauth_provider = OAuthProvider::new(oauth_config);
|
|
||||||
// See examples/servers/complex_auth_sse.rs for complete implementation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Best Practices
|
|
||||||
|
|
||||||
### Async Operations
|
|
||||||
|
|
||||||
Use async/await for non-blocking operations:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[tool]
|
|
||||||
async fn fetch_data(params: Parameters<FetchParams>) -> Result<String, String> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let response = client
|
|
||||||
.get(¶ms.inner().url)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let text = response.text().await.map_err(|e| e.to_string())?;
|
|
||||||
Ok(text)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
|
|
||||||
Use `Arc` and `RwLock` for shared state:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
pub struct ServerState {
|
|
||||||
counter: Arc<RwLock<i32>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
counter: Arc::new(RwLock::new(0)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn increment(&self) -> i32 {
|
|
||||||
let mut counter = self.counter.write().await;
|
|
||||||
*counter += 1;
|
|
||||||
*counter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logging and Tracing
|
|
||||||
|
|
||||||
### Setup Tracing
|
|
||||||
|
|
||||||
Configure tracing for observability:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use tracing::{info, warn, error, debug};
|
|
||||||
use tracing_subscriber;
|
|
||||||
|
|
||||||
fn init_logging() {
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_max_level(tracing::Level::DEBUG)
|
|
||||||
.with_target(false)
|
|
||||||
.with_thread_ids(true)
|
|
||||||
.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tool]
|
|
||||||
async fn my_tool(params: Parameters<MyParams>) -> String {
|
|
||||||
debug!("Tool called with params: {:?}", params);
|
|
||||||
info!("Processing request");
|
|
||||||
|
|
||||||
// Tool logic...
|
|
||||||
|
|
||||||
info!("Request completed");
|
|
||||||
"Done".to_string()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### Binary Distribution
|
|
||||||
|
|
||||||
Build optimized release binaries:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build --release --target x86_64-unknown-linux-gnu
|
|
||||||
cargo build --release --target x86_64-pc-windows-msvc
|
|
||||||
cargo build --release --target x86_64-apple-darwin
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cross-Compilation
|
|
||||||
|
|
||||||
Use cross for cross-platform builds:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo install cross
|
|
||||||
cross build --release --target aarch64-unknown-linux-gnu
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Deployment
|
|
||||||
|
|
||||||
Create a Dockerfile:
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
FROM rust:1.75 as builder
|
|
||||||
WORKDIR /app
|
|
||||||
COPY . .
|
|
||||||
RUN cargo build --release
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
|
||||||
RUN apt-get update && apt-get install -y ca-certificates
|
|
||||||
COPY --from=builder /app/target/release/my-mcp-server /usr/local/bin/
|
|
||||||
CMD ["my-mcp-server"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Additional Resources
|
|
||||||
|
|
||||||
- [rmcp Documentation](https://docs.rs/rmcp)
|
|
||||||
- [rmcp-macros Documentation](https://docs.rs/rmcp-macros)
|
|
||||||
- [Examples Repository](https://github.com/modelcontextprotocol/rust-sdk/tree/main/examples)
|
|
||||||
- [MCP Specification](https://spec.modelcontextprotocol.io/)
|
|
||||||
- [Rust Async Book](https://rust-lang.github.io/async-book/)
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
id: swift-mcp-development
|
|
||||||
name: Swift MCP Server Development
|
|
||||||
description: 'Comprehensive collection for building Model Context Protocol servers in Swift using the official MCP Swift SDK with modern concurrency features.'
|
|
||||||
tags: [swift, mcp, model-context-protocol, server-development, sdk, ios, macos, concurrency, actor, async-await]
|
|
||||||
items:
|
|
||||||
- path: instructions/swift-mcp-server.instructions.md
|
|
||||||
kind: instruction
|
|
||||||
- path: prompts/swift-mcp-server-generator.prompt.md
|
|
||||||
kind: prompt
|
|
||||||
- path: chatmodes/swift-mcp-expert.chatmode.md
|
|
||||||
kind: chat-mode
|
|
||||||
usage: |
|
|
||||||
recommended
|
|
||||||
|
|
||||||
This chat mode provides expert guidance for building MCP servers in Swift.
|
|
||||||
|
|
||||||
This chat mode is ideal for:
|
|
||||||
- Creating new MCP server projects with Swift
|
|
||||||
- Implementing async/await patterns and actor-based concurrency
|
|
||||||
- Setting up stdio, HTTP, or network transports
|
|
||||||
- Debugging Swift concurrency and ServiceLifecycle integration
|
|
||||||
- Learning Swift MCP best practices with the official SDK
|
|
||||||
- Optimizing server performance for iOS/macOS platforms
|
|
||||||
|
|
||||||
To get the best results, consider:
|
|
||||||
- Using the instruction file to set context for Swift MCP development
|
|
||||||
- Using the prompt to generate initial project structure
|
|
||||||
- Switching to the expert chat mode for detailed implementation help
|
|
||||||
- Specifying whether you need stdio, HTTP, or network transport
|
|
||||||
- Providing details about what tools or functionality you need
|
|
||||||
- Mentioning if you need resources, prompts, or special capabilities
|
|
||||||
|
|
||||||
display:
|
|
||||||
ordering: manual
|
|
||||||
show_badge: true
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
# Swift MCP Server Development
|
|
||||||
|
|
||||||
'Comprehensive collection for building Model Context Protocol servers in Swift using the official MCP Swift SDK with modern concurrency features.'
|
|
||||||
|
|
||||||
**Tags:** swift, mcp, model-context-protocol, server-development, sdk, ios, macos, concurrency, actor, async-await
|
|
||||||
|
|
||||||
## Items in this Collection
|
|
||||||
|
|
||||||
| Title | Type | Description |
|
|
||||||
| ----- | ---- | ----------- |
|
|
||||||
| [Swift MCP Server Development Guidelines](../instructions/swift-mcp-server.instructions.md)<br />[](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fswift-mcp-server.instructions.md)<br />[](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fswift-mcp-server.instructions.md) | Instruction | Best practices and patterns for building Model Context Protocol (MCP) servers in Swift using the official MCP Swift SDK package. |
|
|
||||||
| [Swift MCP Server Generator](../prompts/swift-mcp-server-generator.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fswift-mcp-server-generator.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode-insiders%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fswift-mcp-server-generator.prompt.md) | Prompt | Generate a complete Model Context Protocol server project in Swift using the official MCP Swift SDK package. |
|
|
||||||
| [Swift MCP Expert](../chatmodes/swift-mcp-expert.chatmode.md)<br />[](https://aka.ms/awesome-copilot/install/chatmode?url=vscode%3Achat-mode%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fchatmodes%2Fswift-mcp-expert.chatmode.md)<br />[](https://aka.ms/awesome-copilot/install/chatmode?url=vscode-insiders%3Achat-mode%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fchatmodes%2Fswift-mcp-expert.chatmode.md) | Chat Mode | Expert assistance for building Model Context Protocol servers in Swift using modern concurrency features and the official MCP Swift SDK. [see usage](#swift-mcp-expert) |
|
|
||||||
|
|
||||||
## Collection Usage
|
|
||||||
|
|
||||||
### Swift MCP Expert
|
|
||||||
|
|
||||||
recommended
|
|
||||||
|
|
||||||
This chat mode provides expert guidance for building MCP servers in Swift.
|
|
||||||
|
|
||||||
This chat mode is ideal for:
|
|
||||||
- Creating new MCP server projects with Swift
|
|
||||||
- Implementing async/await patterns and actor-based concurrency
|
|
||||||
- Setting up stdio, HTTP, or network transports
|
|
||||||
- Debugging Swift concurrency and ServiceLifecycle integration
|
|
||||||
- Learning Swift MCP best practices with the official SDK
|
|
||||||
- Optimizing server performance for iOS/macOS platforms
|
|
||||||
|
|
||||||
To get the best results, consider:
|
|
||||||
- Using the instruction file to set context for Swift MCP development
|
|
||||||
- Using the prompt to generate initial project structure
|
|
||||||
- Switching to the expert chat mode for detailed implementation help
|
|
||||||
- Specifying whether you need stdio, HTTP, or network transport
|
|
||||||
- Providing details about what tools or functionality you need
|
|
||||||
- Mentioning if you need resources, prompts, or special capabilities
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*This collection includes 3 curated items for swift mcp server development.*
|
|
||||||
@ -1,240 +0,0 @@
|
|||||||
---
|
|
||||||
description: 'Expert assistance for building Model Context Protocol servers in Swift using modern concurrency features and the official MCP Swift SDK.'
|
|
||||||
model: GPT-4.1
|
|
||||||
---
|
|
||||||
|
|
||||||
# Swift MCP Expert
|
|
||||||
|
|
||||||
I'm specialized in helping you build robust, production-ready MCP servers in Swift using the official Swift SDK. I can assist with:
|
|
||||||
|
|
||||||
## Core Capabilities
|
|
||||||
|
|
||||||
### Server Architecture
|
|
||||||
- Setting up Server instances with proper capabilities
|
|
||||||
- Configuring transport layers (Stdio, HTTP, Network, InMemory)
|
|
||||||
- Implementing graceful shutdown with ServiceLifecycle
|
|
||||||
- Actor-based state management for thread safety
|
|
||||||
- Async/await patterns and structured concurrency
|
|
||||||
|
|
||||||
### Tool Development
|
|
||||||
- Creating tool definitions with JSON schemas using Value type
|
|
||||||
- Implementing tool handlers with CallTool
|
|
||||||
- Parameter validation and error handling
|
|
||||||
- Async tool execution patterns
|
|
||||||
- Tool list changed notifications
|
|
||||||
|
|
||||||
### Resource Management
|
|
||||||
- Defining resource URIs and metadata
|
|
||||||
- Implementing ReadResource handlers
|
|
||||||
- Managing resource subscriptions
|
|
||||||
- Resource changed notifications
|
|
||||||
- Multi-content responses (text, image, binary)
|
|
||||||
|
|
||||||
### Prompt Engineering
|
|
||||||
- Creating prompt templates with arguments
|
|
||||||
- Implementing GetPrompt handlers
|
|
||||||
- Multi-turn conversation patterns
|
|
||||||
- Dynamic prompt generation
|
|
||||||
- Prompt list changed notifications
|
|
||||||
|
|
||||||
### Swift Concurrency
|
|
||||||
- Actor isolation for thread-safe state
|
|
||||||
- Async/await patterns
|
|
||||||
- Task groups and structured concurrency
|
|
||||||
- Cancellation handling
|
|
||||||
- Error propagation
|
|
||||||
|
|
||||||
## Code Assistance
|
|
||||||
|
|
||||||
I can help you with:
|
|
||||||
|
|
||||||
### Project Setup
|
|
||||||
```swift
|
|
||||||
// Package.swift with MCP SDK
|
|
||||||
.package(
|
|
||||||
url: "https://github.com/modelcontextprotocol/swift-sdk.git",
|
|
||||||
from: "0.10.0"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Server Creation
|
|
||||||
```swift
|
|
||||||
let server = Server(
|
|
||||||
name: "MyServer",
|
|
||||||
version: "1.0.0",
|
|
||||||
capabilities: .init(
|
|
||||||
prompts: .init(listChanged: true),
|
|
||||||
resources: .init(subscribe: true, listChanged: true),
|
|
||||||
tools: .init(listChanged: true)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Handler Registration
|
|
||||||
```swift
|
|
||||||
await server.withMethodHandler(CallTool.self) { params in
|
|
||||||
// Tool implementation
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Transport Configuration
|
|
||||||
```swift
|
|
||||||
let transport = StdioTransport(logger: logger)
|
|
||||||
try await server.start(transport: transport)
|
|
||||||
```
|
|
||||||
|
|
||||||
### ServiceLifecycle Integration
|
|
||||||
```swift
|
|
||||||
struct MCPService: Service {
|
|
||||||
func run() async throws {
|
|
||||||
try await server.start(transport: transport)
|
|
||||||
}
|
|
||||||
|
|
||||||
func shutdown() async throws {
|
|
||||||
await server.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### Actor-Based State
|
|
||||||
Always use actors for shared mutable state:
|
|
||||||
```swift
|
|
||||||
actor ServerState {
|
|
||||||
private var subscriptions: Set<String> = []
|
|
||||||
|
|
||||||
func addSubscription(_ uri: String) {
|
|
||||||
subscriptions.insert(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
Use proper Swift error handling:
|
|
||||||
```swift
|
|
||||||
do {
|
|
||||||
let result = try performOperation()
|
|
||||||
return .init(content: [.text(result)], isError: false)
|
|
||||||
} catch let error as MCPError {
|
|
||||||
return .init(content: [.text(error.localizedDescription)], isError: true)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logging
|
|
||||||
Use structured logging with swift-log:
|
|
||||||
```swift
|
|
||||||
logger.info("Tool called", metadata: [
|
|
||||||
"name": .string(params.name),
|
|
||||||
"args": .string("\(params.arguments ?? [:])")
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
### JSON Schemas
|
|
||||||
Use the Value type for schemas:
|
|
||||||
```swift
|
|
||||||
.object([
|
|
||||||
"type": .string("object"),
|
|
||||||
"properties": .object([
|
|
||||||
"name": .object([
|
|
||||||
"type": .string("string")
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
"required": .array([.string("name")])
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
### Request/Response Handler
|
|
||||||
```swift
|
|
||||||
await server.withMethodHandler(CallTool.self) { params in
|
|
||||||
guard let arg = params.arguments?["key"]?.stringValue else {
|
|
||||||
throw MCPError.invalidParams("Missing key")
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = await processAsync(arg)
|
|
||||||
|
|
||||||
return .init(
|
|
||||||
content: [.text(result)],
|
|
||||||
isError: false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource Subscription
|
|
||||||
```swift
|
|
||||||
await server.withMethodHandler(ResourceSubscribe.self) { params in
|
|
||||||
await state.addSubscription(params.uri)
|
|
||||||
logger.info("Subscribed to \(params.uri)")
|
|
||||||
return .init()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Concurrent Operations
|
|
||||||
```swift
|
|
||||||
async let result1 = fetchData1()
|
|
||||||
async let result2 = fetchData2()
|
|
||||||
let combined = await "\(result1) and \(result2)"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Initialize Hook
|
|
||||||
```swift
|
|
||||||
try await server.start(transport: transport) { clientInfo, capabilities in
|
|
||||||
logger.info("Client: \(clientInfo.name) v\(clientInfo.version)")
|
|
||||||
|
|
||||||
if capabilities.sampling != nil {
|
|
||||||
logger.info("Client supports sampling")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Platform Support
|
|
||||||
|
|
||||||
The Swift SDK supports:
|
|
||||||
- macOS 13.0+
|
|
||||||
- iOS 16.0+
|
|
||||||
- watchOS 9.0+
|
|
||||||
- tvOS 16.0+
|
|
||||||
- visionOS 1.0+
|
|
||||||
- Linux (glibc and musl)
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Write async tests:
|
|
||||||
```swift
|
|
||||||
func testTool() async throws {
|
|
||||||
let params = CallTool.Params(
|
|
||||||
name: "test",
|
|
||||||
arguments: ["key": .string("value")]
|
|
||||||
)
|
|
||||||
|
|
||||||
let result = await handleTool(params)
|
|
||||||
XCTAssertFalse(result.isError ?? true)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Debugging
|
|
||||||
|
|
||||||
Enable debug logging:
|
|
||||||
```swift
|
|
||||||
var logger = Logger(label: "com.example.mcp-server")
|
|
||||||
logger.logLevel = .debug
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ask Me About
|
|
||||||
|
|
||||||
- Server setup and configuration
|
|
||||||
- Tool, resource, and prompt implementations
|
|
||||||
- Swift concurrency patterns
|
|
||||||
- Actor-based state management
|
|
||||||
- ServiceLifecycle integration
|
|
||||||
- Transport configuration (Stdio, HTTP, Network)
|
|
||||||
- JSON schema construction
|
|
||||||
- Error handling strategies
|
|
||||||
- Testing async code
|
|
||||||
- Platform-specific considerations
|
|
||||||
- Performance optimization
|
|
||||||
- Deployment strategies
|
|
||||||
|
|
||||||
I'm here to help you build efficient, safe, and idiomatic Swift MCP servers. What would you like to work on?
|
|
||||||
@ -1,669 +0,0 @@
|
|||||||
---
|
|
||||||
description: 'Generate a complete Model Context Protocol server project in Swift using the official MCP Swift SDK package.'
|
|
||||||
mode: agent
|
|
||||||
---
|
|
||||||
|
|
||||||
# Swift MCP Server Generator
|
|
||||||
|
|
||||||
Generate a complete, production-ready MCP server in Swift using the official Swift SDK package.
|
|
||||||
|
|
||||||
## Project Generation
|
|
||||||
|
|
||||||
When asked to create a Swift MCP server, generate a complete project with this structure:
|
|
||||||
|
|
||||||
```
|
|
||||||
my-mcp-server/
|
|
||||||
├── Package.swift
|
|
||||||
├── Sources/
|
|
||||||
│ └── MyMCPServer/
|
|
||||||
│ ├── main.swift
|
|
||||||
│ ├── Server.swift
|
|
||||||
│ ├── Tools/
|
|
||||||
│ │ ├── ToolDefinitions.swift
|
|
||||||
│ │ └── ToolHandlers.swift
|
|
||||||
│ ├── Resources/
|
|
||||||
│ │ ├── ResourceDefinitions.swift
|
|
||||||
│ │ └── ResourceHandlers.swift
|
|
||||||
│ └── Prompts/
|
|
||||||
│ ├── PromptDefinitions.swift
|
|
||||||
│ └── PromptHandlers.swift
|
|
||||||
├── Tests/
|
|
||||||
│ └── MyMCPServerTests/
|
|
||||||
│ └── ServerTests.swift
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Package.swift Template
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// swift-tools-version: 6.0
|
|
||||||
import PackageDescription
|
|
||||||
|
|
||||||
let package = Package(
|
|
||||||
name: "MyMCPServer",
|
|
||||||
platforms: [
|
|
||||||
.macOS(.v13),
|
|
||||||
.iOS(.v16),
|
|
||||||
.watchOS(.v9),
|
|
||||||
.tvOS(.v16),
|
|
||||||
.visionOS(.v1)
|
|
||||||
],
|
|
||||||
dependencies: [
|
|
||||||
.package(
|
|
||||||
url: "https://github.com/modelcontextprotocol/swift-sdk.git",
|
|
||||||
from: "0.10.0"
|
|
||||||
),
|
|
||||||
.package(
|
|
||||||
url: "https://github.com/apple/swift-log.git",
|
|
||||||
from: "1.5.0"
|
|
||||||
),
|
|
||||||
.package(
|
|
||||||
url: "https://github.com/swift-server/swift-service-lifecycle.git",
|
|
||||||
from: "2.0.0"
|
|
||||||
)
|
|
||||||
],
|
|
||||||
targets: [
|
|
||||||
.executableTarget(
|
|
||||||
name: "MyMCPServer",
|
|
||||||
dependencies: [
|
|
||||||
.product(name: "MCP", package: "swift-sdk"),
|
|
||||||
.product(name: "Logging", package: "swift-log"),
|
|
||||||
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle")
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.testTarget(
|
|
||||||
name: "MyMCPServerTests",
|
|
||||||
dependencies: ["MyMCPServer"]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## main.swift Template
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import MCP
|
|
||||||
import Logging
|
|
||||||
import ServiceLifecycle
|
|
||||||
|
|
||||||
struct MCPService: Service {
|
|
||||||
let server: Server
|
|
||||||
let transport: Transport
|
|
||||||
|
|
||||||
func run() async throws {
|
|
||||||
try await server.start(transport: transport) { clientInfo, capabilities in
|
|
||||||
logger.info("Client connected", metadata: [
|
|
||||||
"name": .string(clientInfo.name),
|
|
||||||
"version": .string(clientInfo.version)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep service running
|
|
||||||
try await Task.sleep(for: .days(365 * 100))
|
|
||||||
}
|
|
||||||
|
|
||||||
func shutdown() async throws {
|
|
||||||
logger.info("Shutting down MCP server")
|
|
||||||
await server.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var logger = Logger(label: "com.example.mcp-server")
|
|
||||||
logger.logLevel = .info
|
|
||||||
|
|
||||||
do {
|
|
||||||
let server = await createServer()
|
|
||||||
let transport = StdioTransport(logger: logger)
|
|
||||||
let service = MCPService(server: server, transport: transport)
|
|
||||||
|
|
||||||
let serviceGroup = ServiceGroup(
|
|
||||||
services: [service],
|
|
||||||
configuration: .init(
|
|
||||||
gracefulShutdownSignals: [.sigterm, .sigint]
|
|
||||||
),
|
|
||||||
logger: logger
|
|
||||||
)
|
|
||||||
|
|
||||||
try await serviceGroup.run()
|
|
||||||
} catch {
|
|
||||||
logger.error("Fatal error", metadata: ["error": .string("\(error)")])
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server.swift Template
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import MCP
|
|
||||||
import Logging
|
|
||||||
|
|
||||||
func createServer() async -> Server {
|
|
||||||
let server = Server(
|
|
||||||
name: "MyMCPServer",
|
|
||||||
version: "1.0.0",
|
|
||||||
capabilities: .init(
|
|
||||||
prompts: .init(listChanged: true),
|
|
||||||
resources: .init(subscribe: true, listChanged: true),
|
|
||||||
tools: .init(listChanged: true)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register tool handlers
|
|
||||||
await registerToolHandlers(server: server)
|
|
||||||
|
|
||||||
// Register resource handlers
|
|
||||||
await registerResourceHandlers(server: server)
|
|
||||||
|
|
||||||
// Register prompt handlers
|
|
||||||
await registerPromptHandlers(server: server)
|
|
||||||
|
|
||||||
return server
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ToolDefinitions.swift Template
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import MCP
|
|
||||||
|
|
||||||
func getToolDefinitions() -> [Tool] {
|
|
||||||
[
|
|
||||||
Tool(
|
|
||||||
name: "greet",
|
|
||||||
description: "Generate a greeting message",
|
|
||||||
inputSchema: .object([
|
|
||||||
"type": .string("object"),
|
|
||||||
"properties": .object([
|
|
||||||
"name": .object([
|
|
||||||
"type": .string("string"),
|
|
||||||
"description": .string("Name to greet")
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
"required": .array([.string("name")])
|
|
||||||
])
|
|
||||||
),
|
|
||||||
Tool(
|
|
||||||
name: "calculate",
|
|
||||||
description: "Perform mathematical calculations",
|
|
||||||
inputSchema: .object([
|
|
||||||
"type": .string("object"),
|
|
||||||
"properties": .object([
|
|
||||||
"operation": .object([
|
|
||||||
"type": .string("string"),
|
|
||||||
"enum": .array([
|
|
||||||
.string("add"),
|
|
||||||
.string("subtract"),
|
|
||||||
.string("multiply"),
|
|
||||||
.string("divide")
|
|
||||||
]),
|
|
||||||
"description": .string("Operation to perform")
|
|
||||||
]),
|
|
||||||
"a": .object([
|
|
||||||
"type": .string("number"),
|
|
||||||
"description": .string("First operand")
|
|
||||||
]),
|
|
||||||
"b": .object([
|
|
||||||
"type": .string("number"),
|
|
||||||
"description": .string("Second operand")
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
"required": .array([
|
|
||||||
.string("operation"),
|
|
||||||
.string("a"),
|
|
||||||
.string("b")
|
|
||||||
])
|
|
||||||
])
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ToolHandlers.swift Template
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import MCP
|
|
||||||
import Logging
|
|
||||||
|
|
||||||
private let logger = Logger(label: "com.example.mcp-server.tools")
|
|
||||||
|
|
||||||
func registerToolHandlers(server: Server) async {
|
|
||||||
await server.withMethodHandler(ListTools.self) { _ in
|
|
||||||
logger.debug("Listing available tools")
|
|
||||||
return .init(tools: getToolDefinitions())
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.withMethodHandler(CallTool.self) { params in
|
|
||||||
logger.info("Tool called", metadata: ["name": .string(params.name)])
|
|
||||||
|
|
||||||
switch params.name {
|
|
||||||
case "greet":
|
|
||||||
return handleGreet(params: params)
|
|
||||||
|
|
||||||
case "calculate":
|
|
||||||
return handleCalculate(params: params)
|
|
||||||
|
|
||||||
default:
|
|
||||||
logger.warning("Unknown tool requested", metadata: ["name": .string(params.name)])
|
|
||||||
return .init(
|
|
||||||
content: [.text("Unknown tool: \(params.name)")],
|
|
||||||
isError: true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleGreet(params: CallTool.Params) -> CallTool.Result {
|
|
||||||
guard let name = params.arguments?["name"]?.stringValue else {
|
|
||||||
return .init(
|
|
||||||
content: [.text("Missing 'name' parameter")],
|
|
||||||
isError: true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let greeting = "Hello, \(name)! Welcome to MCP."
|
|
||||||
logger.debug("Generated greeting", metadata: ["name": .string(name)])
|
|
||||||
|
|
||||||
return .init(
|
|
||||||
content: [.text(greeting)],
|
|
||||||
isError: false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleCalculate(params: CallTool.Params) -> CallTool.Result {
|
|
||||||
guard let operation = params.arguments?["operation"]?.stringValue,
|
|
||||||
let a = params.arguments?["a"]?.doubleValue,
|
|
||||||
let b = params.arguments?["b"]?.doubleValue else {
|
|
||||||
return .init(
|
|
||||||
content: [.text("Missing or invalid parameters")],
|
|
||||||
isError: true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: Double
|
|
||||||
switch operation {
|
|
||||||
case "add":
|
|
||||||
result = a + b
|
|
||||||
case "subtract":
|
|
||||||
result = a - b
|
|
||||||
case "multiply":
|
|
||||||
result = a * b
|
|
||||||
case "divide":
|
|
||||||
guard b != 0 else {
|
|
||||||
return .init(
|
|
||||||
content: [.text("Division by zero")],
|
|
||||||
isError: true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
result = a / b
|
|
||||||
default:
|
|
||||||
return .init(
|
|
||||||
content: [.text("Unknown operation: \(operation)")],
|
|
||||||
isError: true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("Calculation performed", metadata: [
|
|
||||||
"operation": .string(operation),
|
|
||||||
"result": .string("\(result)")
|
|
||||||
])
|
|
||||||
|
|
||||||
return .init(
|
|
||||||
content: [.text("Result: \(result)")],
|
|
||||||
isError: false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ResourceDefinitions.swift Template
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import MCP
|
|
||||||
|
|
||||||
func getResourceDefinitions() -> [Resource] {
|
|
||||||
[
|
|
||||||
Resource(
|
|
||||||
name: "Example Data",
|
|
||||||
uri: "resource://data/example",
|
|
||||||
description: "Example resource data",
|
|
||||||
mimeType: "application/json"
|
|
||||||
),
|
|
||||||
Resource(
|
|
||||||
name: "Configuration",
|
|
||||||
uri: "resource://config",
|
|
||||||
description: "Server configuration",
|
|
||||||
mimeType: "application/json"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ResourceHandlers.swift Template
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import MCP
|
|
||||||
import Logging
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
private let logger = Logger(label: "com.example.mcp-server.resources")
|
|
||||||
|
|
||||||
actor ResourceState {
|
|
||||||
private var subscriptions: Set<String> = []
|
|
||||||
|
|
||||||
func addSubscription(_ uri: String) {
|
|
||||||
subscriptions.insert(uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeSubscription(_ uri: String) {
|
|
||||||
subscriptions.remove(uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSubscribed(_ uri: String) -> Bool {
|
|
||||||
subscriptions.contains(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private let state = ResourceState()
|
|
||||||
|
|
||||||
func registerResourceHandlers(server: Server) async {
|
|
||||||
await server.withMethodHandler(ListResources.self) { params in
|
|
||||||
logger.debug("Listing available resources")
|
|
||||||
return .init(resources: getResourceDefinitions(), nextCursor: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.withMethodHandler(ReadResource.self) { params in
|
|
||||||
logger.info("Reading resource", metadata: ["uri": .string(params.uri)])
|
|
||||||
|
|
||||||
switch params.uri {
|
|
||||||
case "resource://data/example":
|
|
||||||
let jsonData = """
|
|
||||||
{
|
|
||||||
"message": "Example resource data",
|
|
||||||
"timestamp": "\(Date())"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
return .init(contents: [
|
|
||||||
.text(jsonData, uri: params.uri, mimeType: "application/json")
|
|
||||||
])
|
|
||||||
|
|
||||||
case "resource://config":
|
|
||||||
let config = """
|
|
||||||
{
|
|
||||||
"serverName": "MyMCPServer",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
return .init(contents: [
|
|
||||||
.text(config, uri: params.uri, mimeType: "application/json")
|
|
||||||
])
|
|
||||||
|
|
||||||
default:
|
|
||||||
logger.warning("Unknown resource requested", metadata: ["uri": .string(params.uri)])
|
|
||||||
throw MCPError.invalidParams("Unknown resource URI: \(params.uri)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.withMethodHandler(ResourceSubscribe.self) { params in
|
|
||||||
logger.info("Client subscribed to resource", metadata: ["uri": .string(params.uri)])
|
|
||||||
await state.addSubscription(params.uri)
|
|
||||||
return .init()
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.withMethodHandler(ResourceUnsubscribe.self) { params in
|
|
||||||
logger.info("Client unsubscribed from resource", metadata: ["uri": .string(params.uri)])
|
|
||||||
await state.removeSubscription(params.uri)
|
|
||||||
return .init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## PromptDefinitions.swift Template
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import MCP
|
|
||||||
|
|
||||||
func getPromptDefinitions() -> [Prompt] {
|
|
||||||
[
|
|
||||||
Prompt(
|
|
||||||
name: "code-review",
|
|
||||||
description: "Generate a code review prompt",
|
|
||||||
arguments: [
|
|
||||||
.init(name: "language", description: "Programming language", required: true),
|
|
||||||
.init(name: "focus", description: "Review focus area", required: false)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## PromptHandlers.swift Template
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import MCP
|
|
||||||
import Logging
|
|
||||||
|
|
||||||
private let logger = Logger(label: "com.example.mcp-server.prompts")
|
|
||||||
|
|
||||||
func registerPromptHandlers(server: Server) async {
|
|
||||||
await server.withMethodHandler(ListPrompts.self) { params in
|
|
||||||
logger.debug("Listing available prompts")
|
|
||||||
return .init(prompts: getPromptDefinitions(), nextCursor: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.withMethodHandler(GetPrompt.self) { params in
|
|
||||||
logger.info("Getting prompt", metadata: ["name": .string(params.name)])
|
|
||||||
|
|
||||||
switch params.name {
|
|
||||||
case "code-review":
|
|
||||||
return handleCodeReviewPrompt(params: params)
|
|
||||||
|
|
||||||
default:
|
|
||||||
logger.warning("Unknown prompt requested", metadata: ["name": .string(params.name)])
|
|
||||||
throw MCPError.invalidParams("Unknown prompt: \(params.name)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleCodeReviewPrompt(params: GetPrompt.Params) -> GetPrompt.Result {
|
|
||||||
guard let language = params.arguments?["language"]?.stringValue else {
|
|
||||||
return .init(
|
|
||||||
description: "Missing language parameter",
|
|
||||||
messages: []
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let focus = params.arguments?["focus"]?.stringValue ?? "general quality"
|
|
||||||
|
|
||||||
let description = "Code review for \(language) with focus on \(focus)"
|
|
||||||
let messages: [Prompt.Message] = [
|
|
||||||
.user("Please review this \(language) code with focus on \(focus)."),
|
|
||||||
.assistant("I'll review the code focusing on \(focus). Please share the code."),
|
|
||||||
.user("Here's the code to review: [paste code here]")
|
|
||||||
]
|
|
||||||
|
|
||||||
logger.debug("Generated code review prompt", metadata: [
|
|
||||||
"language": .string(language),
|
|
||||||
"focus": .string(focus)
|
|
||||||
])
|
|
||||||
|
|
||||||
return .init(description: description, messages: messages)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ServerTests.swift Template
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import XCTest
|
|
||||||
@testable import MyMCPServer
|
|
||||||
|
|
||||||
final class ServerTests: XCTestCase {
|
|
||||||
func testGreetTool() async throws {
|
|
||||||
let params = CallTool.Params(
|
|
||||||
name: "greet",
|
|
||||||
arguments: ["name": .string("Swift")]
|
|
||||||
)
|
|
||||||
|
|
||||||
let result = handleGreet(params: params)
|
|
||||||
|
|
||||||
XCTAssertFalse(result.isError ?? true)
|
|
||||||
XCTAssertEqual(result.content.count, 1)
|
|
||||||
|
|
||||||
if case .text(let message) = result.content[0] {
|
|
||||||
XCTAssertTrue(message.contains("Swift"))
|
|
||||||
} else {
|
|
||||||
XCTFail("Expected text content")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testCalculateTool() async throws {
|
|
||||||
let params = CallTool.Params(
|
|
||||||
name: "calculate",
|
|
||||||
arguments: [
|
|
||||||
"operation": .string("add"),
|
|
||||||
"a": .number(5),
|
|
||||||
"b": .number(3)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
let result = handleCalculate(params: params)
|
|
||||||
|
|
||||||
XCTAssertFalse(result.isError ?? true)
|
|
||||||
XCTAssertEqual(result.content.count, 1)
|
|
||||||
|
|
||||||
if case .text(let message) = result.content[0] {
|
|
||||||
XCTAssertTrue(message.contains("8"))
|
|
||||||
} else {
|
|
||||||
XCTFail("Expected text content")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDivideByZero() async throws {
|
|
||||||
let params = CallTool.Params(
|
|
||||||
name: "calculate",
|
|
||||||
arguments: [
|
|
||||||
"operation": .string("divide"),
|
|
||||||
"a": .number(10),
|
|
||||||
"b": .number(0)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
let result = handleCalculate(params: params)
|
|
||||||
|
|
||||||
XCTAssertTrue(result.isError ?? false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## README.md Template
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# MyMCPServer
|
|
||||||
|
|
||||||
A Model Context Protocol server built with Swift.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- ✅ Tools: greet, calculate
|
|
||||||
- ✅ Resources: example data, configuration
|
|
||||||
- ✅ Prompts: code-review
|
|
||||||
- ✅ Graceful shutdown with ServiceLifecycle
|
|
||||||
- ✅ Structured logging with swift-log
|
|
||||||
- ✅ Full test coverage
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Swift 6.0+
|
|
||||||
- macOS 13+, iOS 16+, or Linux
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
swift build -c release
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Run the server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
swift run
|
|
||||||
```
|
|
||||||
|
|
||||||
Or with logging:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
LOG_LEVEL=debug swift run
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
swift test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
The server uses:
|
|
||||||
- [MCP Swift SDK](https://github.com/modelcontextprotocol/swift-sdk) - MCP protocol implementation
|
|
||||||
- [swift-log](https://github.com/apple/swift-log) - Structured logging
|
|
||||||
- [swift-service-lifecycle](https://github.com/swift-server/swift-service-lifecycle) - Graceful shutdown
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
- `Sources/MyMCPServer/main.swift` - Entry point with ServiceLifecycle
|
|
||||||
- `Sources/MyMCPServer/Server.swift` - Server configuration
|
|
||||||
- `Sources/MyMCPServer/Tools/` - Tool definitions and handlers
|
|
||||||
- `Sources/MyMCPServer/Resources/` - Resource definitions and handlers
|
|
||||||
- `Sources/MyMCPServer/Prompts/` - Prompt definitions and handlers
|
|
||||||
- `Tests/` - Unit tests
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
```
|
|
||||||
|
|
||||||
## Generation Instructions
|
|
||||||
|
|
||||||
1. **Ask for project name and description**
|
|
||||||
2. **Generate all files** with proper naming
|
|
||||||
3. **Use actor-based state** for thread safety
|
|
||||||
4. **Include comprehensive logging** with swift-log
|
|
||||||
5. **Implement graceful shutdown** with ServiceLifecycle
|
|
||||||
6. **Add tests** for all handlers
|
|
||||||
7. **Use modern Swift concurrency** (async/await)
|
|
||||||
8. **Follow Swift naming conventions** (camelCase, PascalCase)
|
|
||||||
9. **Include error handling** with proper MCPError usage
|
|
||||||
10. **Document public APIs** with doc comments
|
|
||||||
|
|
||||||
## Build and Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build
|
|
||||||
swift build
|
|
||||||
|
|
||||||
# Run
|
|
||||||
swift run
|
|
||||||
|
|
||||||
# Test
|
|
||||||
swift test
|
|
||||||
|
|
||||||
# Release build
|
|
||||||
swift build -c release
|
|
||||||
|
|
||||||
# Install
|
|
||||||
swift build -c release
|
|
||||||
cp .build/release/MyMCPServer /usr/local/bin/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration with Claude Desktop
|
|
||||||
|
|
||||||
Add to `claude_desktop_config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"my-mcp-server": {
|
|
||||||
"command": "/path/to/MyMCPServer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,498 +0,0 @@
|
|||||||
---
|
|
||||||
description: 'Best practices and patterns for building Model Context Protocol (MCP) servers in Swift using the official MCP Swift SDK package.'
|
|
||||||
applyTo: "**/*.swift, **/Package.swift, **/Package.resolved"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Swift MCP Server Development Guidelines
|
|
||||||
|
|
||||||
When building MCP servers in Swift, follow these best practices and patterns using the official Swift SDK.
|
|
||||||
|
|
||||||
## Server Setup
|
|
||||||
|
|
||||||
Create an MCP server using the `Server` class with capabilities:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import MCP
|
|
||||||
|
|
||||||
let server = Server(
|
|
||||||
name: "MyServer",
|
|
||||||
version: "1.0.0",
|
|
||||||
capabilities: .init(
|
|
||||||
prompts: .init(listChanged: true),
|
|
||||||
resources: .init(subscribe: true, listChanged: true),
|
|
||||||
tools: .init(listChanged: true)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding Tools
|
|
||||||
|
|
||||||
Use `withMethodHandler` to register tool handlers:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// Register tool list handler
|
|
||||||
await server.withMethodHandler(ListTools.self) { _ in
|
|
||||||
let tools = [
|
|
||||||
Tool(
|
|
||||||
name: "search",
|
|
||||||
description: "Search for information",
|
|
||||||
inputSchema: .object([
|
|
||||||
"properties": .object([
|
|
||||||
"query": .string("Search query"),
|
|
||||||
"limit": .number("Maximum results")
|
|
||||||
]),
|
|
||||||
"required": .array([.string("query")])
|
|
||||||
])
|
|
||||||
)
|
|
||||||
]
|
|
||||||
return .init(tools: tools)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register tool call handler
|
|
||||||
await server.withMethodHandler(CallTool.self) { params in
|
|
||||||
switch params.name {
|
|
||||||
case "search":
|
|
||||||
let query = params.arguments?["query"]?.stringValue ?? ""
|
|
||||||
let limit = params.arguments?["limit"]?.intValue ?? 10
|
|
||||||
|
|
||||||
// Perform search
|
|
||||||
let results = performSearch(query: query, limit: limit)
|
|
||||||
|
|
||||||
return .init(
|
|
||||||
content: [.text("Found \(results.count) results")],
|
|
||||||
isError: false
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return .init(
|
|
||||||
content: [.text("Unknown tool")],
|
|
||||||
isError: true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding Resources
|
|
||||||
|
|
||||||
Implement resource handlers for data access:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// Register resource list handler
|
|
||||||
await server.withMethodHandler(ListResources.self) { params in
|
|
||||||
let resources = [
|
|
||||||
Resource(
|
|
||||||
name: "Data File",
|
|
||||||
uri: "resource://data/example.txt",
|
|
||||||
description: "Example data file",
|
|
||||||
mimeType: "text/plain"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
return .init(resources: resources, nextCursor: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register resource read handler
|
|
||||||
await server.withMethodHandler(ReadResource.self) { params in
|
|
||||||
switch params.uri {
|
|
||||||
case "resource://data/example.txt":
|
|
||||||
let content = loadResourceContent(uri: params.uri)
|
|
||||||
return .init(contents: [
|
|
||||||
Resource.Content.text(
|
|
||||||
content,
|
|
||||||
uri: params.uri,
|
|
||||||
mimeType: "text/plain"
|
|
||||||
)
|
|
||||||
])
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw MCPError.invalidParams("Unknown resource URI: \(params.uri)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register resource subscribe handler
|
|
||||||
await server.withMethodHandler(ResourceSubscribe.self) { params in
|
|
||||||
// Track subscription for notifications
|
|
||||||
subscriptions.insert(params.uri)
|
|
||||||
print("Client subscribed to \(params.uri)")
|
|
||||||
return .init()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding Prompts
|
|
||||||
|
|
||||||
Implement prompt handlers for templated conversations:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// Register prompt list handler
|
|
||||||
await server.withMethodHandler(ListPrompts.self) { params in
|
|
||||||
let prompts = [
|
|
||||||
Prompt(
|
|
||||||
name: "analyze",
|
|
||||||
description: "Analyze a topic",
|
|
||||||
arguments: [
|
|
||||||
.init(name: "topic", description: "Topic to analyze", required: true),
|
|
||||||
.init(name: "depth", description: "Analysis depth", required: false)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
return .init(prompts: prompts, nextCursor: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register prompt get handler
|
|
||||||
await server.withMethodHandler(GetPrompt.self) { params in
|
|
||||||
switch params.name {
|
|
||||||
case "analyze":
|
|
||||||
let topic = params.arguments?["topic"]?.stringValue ?? "general"
|
|
||||||
let depth = params.arguments?["depth"]?.stringValue ?? "basic"
|
|
||||||
|
|
||||||
let description = "Analysis of \(topic) at \(depth) level"
|
|
||||||
let messages: [Prompt.Message] = [
|
|
||||||
.user("Please analyze this topic: \(topic)"),
|
|
||||||
.assistant("I'll provide a \(depth) analysis of \(topic)")
|
|
||||||
]
|
|
||||||
|
|
||||||
return .init(description: description, messages: messages)
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw MCPError.invalidParams("Unknown prompt: \(params.name)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Transport Configuration
|
|
||||||
|
|
||||||
### Stdio Transport
|
|
||||||
|
|
||||||
For local subprocess communication:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import MCP
|
|
||||||
import Logging
|
|
||||||
|
|
||||||
let logger = Logger(label: "com.example.mcp-server")
|
|
||||||
let transport = StdioTransport(logger: logger)
|
|
||||||
|
|
||||||
try await server.start(transport: transport)
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTTP Transport (Client Side)
|
|
||||||
|
|
||||||
For remote server connections:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
let transport = HTTPClientTransport(
|
|
||||||
endpoint: URL(string: "http://localhost:8080")!,
|
|
||||||
streaming: true // Enable Server-Sent Events
|
|
||||||
)
|
|
||||||
|
|
||||||
try await client.connect(transport: transport)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Concurrency and Actors
|
|
||||||
|
|
||||||
The server is an actor, ensuring thread-safe access:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
actor ServerState {
|
|
||||||
private var subscriptions: Set<String> = []
|
|
||||||
private var cache: [String: Any] = [:]
|
|
||||||
|
|
||||||
func addSubscription(_ uri: String) {
|
|
||||||
subscriptions.insert(uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSubscriptions() -> Set<String> {
|
|
||||||
return subscriptions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let state = ServerState()
|
|
||||||
|
|
||||||
await server.withMethodHandler(ResourceSubscribe.self) { params in
|
|
||||||
await state.addSubscription(params.uri)
|
|
||||||
return .init()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
Use Swift's error handling with `MCPError`:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
await server.withMethodHandler(CallTool.self) { params in
|
|
||||||
do {
|
|
||||||
guard let query = params.arguments?["query"]?.stringValue else {
|
|
||||||
throw MCPError.invalidParams("Missing query parameter")
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = try performOperation(query: query)
|
|
||||||
|
|
||||||
return .init(
|
|
||||||
content: [.text(result)],
|
|
||||||
isError: false
|
|
||||||
)
|
|
||||||
} catch let error as MCPError {
|
|
||||||
return .init(
|
|
||||||
content: [.text(error.localizedDescription)],
|
|
||||||
isError: true
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return .init(
|
|
||||||
content: [.text("Unexpected error: \(error.localizedDescription)")],
|
|
||||||
isError: true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## JSON Schema with Value Type
|
|
||||||
|
|
||||||
Use the `Value` type for JSON schemas:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
let schema = Value.object([
|
|
||||||
"type": .string("object"),
|
|
||||||
"properties": .object([
|
|
||||||
"name": .object([
|
|
||||||
"type": .string("string"),
|
|
||||||
"description": .string("User's name")
|
|
||||||
]),
|
|
||||||
"age": .object([
|
|
||||||
"type": .string("integer"),
|
|
||||||
"minimum": .number(0),
|
|
||||||
"maximum": .number(150)
|
|
||||||
]),
|
|
||||||
"email": .object([
|
|
||||||
"type": .string("string"),
|
|
||||||
"format": .string("email")
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
"required": .array([.string("name")])
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
## Swift Package Manager Setup
|
|
||||||
|
|
||||||
Create your `Package.swift`:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// swift-tools-version: 6.0
|
|
||||||
import PackageDescription
|
|
||||||
|
|
||||||
let package = Package(
|
|
||||||
name: "MyMCPServer",
|
|
||||||
platforms: [
|
|
||||||
.macOS(.v13),
|
|
||||||
.iOS(.v16)
|
|
||||||
],
|
|
||||||
dependencies: [
|
|
||||||
.package(
|
|
||||||
url: "https://github.com/modelcontextprotocol/swift-sdk.git",
|
|
||||||
from: "0.10.0"
|
|
||||||
),
|
|
||||||
.package(
|
|
||||||
url: "https://github.com/apple/swift-log.git",
|
|
||||||
from: "1.5.0"
|
|
||||||
)
|
|
||||||
],
|
|
||||||
targets: [
|
|
||||||
.executableTarget(
|
|
||||||
name: "MyMCPServer",
|
|
||||||
dependencies: [
|
|
||||||
.product(name: "MCP", package: "swift-sdk"),
|
|
||||||
.product(name: "Logging", package: "swift-log")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Graceful Shutdown with ServiceLifecycle
|
|
||||||
|
|
||||||
Use Swift Service Lifecycle for proper shutdown:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import MCP
|
|
||||||
import ServiceLifecycle
|
|
||||||
import Logging
|
|
||||||
|
|
||||||
struct MCPService: Service {
|
|
||||||
let server: Server
|
|
||||||
let transport: Transport
|
|
||||||
|
|
||||||
func run() async throws {
|
|
||||||
try await server.start(transport: transport)
|
|
||||||
try await Task.sleep(for: .days(365 * 100))
|
|
||||||
}
|
|
||||||
|
|
||||||
func shutdown() async throws {
|
|
||||||
await server.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let logger = Logger(label: "com.example.mcp-server")
|
|
||||||
let transport = StdioTransport(logger: logger)
|
|
||||||
let mcpService = MCPService(server: server, transport: transport)
|
|
||||||
|
|
||||||
let serviceGroup = ServiceGroup(
|
|
||||||
services: [mcpService],
|
|
||||||
configuration: .init(
|
|
||||||
gracefulShutdownSignals: [.sigterm, .sigint]
|
|
||||||
),
|
|
||||||
logger: logger
|
|
||||||
)
|
|
||||||
|
|
||||||
try await serviceGroup.run()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Async/Await Patterns
|
|
||||||
|
|
||||||
All server operations use Swift concurrency:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
await server.withMethodHandler(CallTool.self) { params in
|
|
||||||
async let result1 = fetchData1()
|
|
||||||
async let result2 = fetchData2()
|
|
||||||
|
|
||||||
let combined = await "\(result1) and \(result2)"
|
|
||||||
|
|
||||||
return .init(
|
|
||||||
content: [.text(combined)],
|
|
||||||
isError: false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
|
|
||||||
Use swift-log for structured logging:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import Logging
|
|
||||||
|
|
||||||
let logger = Logger(label: "com.example.mcp-server")
|
|
||||||
|
|
||||||
await server.withMethodHandler(CallTool.self) { params in
|
|
||||||
logger.info("Tool called", metadata: [
|
|
||||||
"name": .string(params.name),
|
|
||||||
"args": .string("\(params.arguments ?? [:])")
|
|
||||||
])
|
|
||||||
|
|
||||||
// Process tool call
|
|
||||||
|
|
||||||
logger.debug("Tool completed successfully")
|
|
||||||
|
|
||||||
return .init(content: [.text("Result")], isError: false)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Test your server with async/await:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import XCTest
|
|
||||||
@testable import MyMCPServer
|
|
||||||
|
|
||||||
final class ServerTests: XCTestCase {
|
|
||||||
func testToolCall() async throws {
|
|
||||||
let server = createTestServer()
|
|
||||||
|
|
||||||
// Test tool call logic
|
|
||||||
let params = CallTool.Params(
|
|
||||||
name: "search",
|
|
||||||
arguments: ["query": .string("test")]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify behavior
|
|
||||||
XCTAssertNoThrow(try await processToolCall(params))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Initialize Hook
|
|
||||||
|
|
||||||
Validate client connections with an initialize hook:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
try await server.start(transport: transport) { clientInfo, clientCapabilities in
|
|
||||||
// Validate client
|
|
||||||
guard clientInfo.name != "BlockedClient" else {
|
|
||||||
throw MCPError.invalidRequest("Client not allowed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check capabilities
|
|
||||||
if clientCapabilities.sampling == nil {
|
|
||||||
logger.warning("Client doesn't support sampling")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Client connected", metadata: [
|
|
||||||
"name": .string(clientInfo.name),
|
|
||||||
"version": .string(clientInfo.version)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
### Content Types
|
|
||||||
|
|
||||||
Handle different content types:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
return .init(
|
|
||||||
content: [
|
|
||||||
.text("Plain text response"),
|
|
||||||
.image(imageData, mimeType: "image/png", metadata: [
|
|
||||||
"width": 1024,
|
|
||||||
"height": 768
|
|
||||||
]),
|
|
||||||
.resource(
|
|
||||||
uri: "resource://data",
|
|
||||||
mimeType: "application/json",
|
|
||||||
text: jsonString
|
|
||||||
)
|
|
||||||
],
|
|
||||||
isError: false
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Strict Configuration
|
|
||||||
|
|
||||||
Use strict mode to fail fast on missing capabilities:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
let client = Client(
|
|
||||||
name: "StrictClient",
|
|
||||||
version: "1.0.0",
|
|
||||||
configuration: .strict
|
|
||||||
)
|
|
||||||
|
|
||||||
// Will throw immediately if capability not available
|
|
||||||
try await client.listTools()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Request Batching
|
|
||||||
|
|
||||||
Send multiple requests efficiently:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
var tasks: [Task<CallTool.Result, Error>] = []
|
|
||||||
|
|
||||||
try await client.withBatch { batch in
|
|
||||||
for i in 0..<10 {
|
|
||||||
tasks.append(
|
|
||||||
try await batch.addRequest(
|
|
||||||
CallTool.request(.init(
|
|
||||||
name: "process",
|
|
||||||
arguments: ["id": .number(Double(i))]
|
|
||||||
))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (index, task) in tasks.enumerated() {
|
|
||||||
let result = try await task.value
|
|
||||||
print("\(index): \(result.content)")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Loading…
x
Reference in New Issue
Block a user