Building MCP Servers
Create Model Context Protocol servers for AI tool execution
On this page
This guide shows how to create MCP (Model Context Protocol) servers that provide tools for AI clients like Claude Desktop and ChatGPT.
Overview
MCP servers expose tools and resources that AI clients can discover and use. SystemPrompt provides HTTP-native transports with built-in OAuth2 authentication.
Prerequisites
- SystemPrompt project setup
- Understanding of the MCP crate
- Rust 1.75+
Project Setup
Create your MCP server in extensions/mcp/:
cd your-project/extensions/mcp
cargo new --lib my-tools
Update Cargo.toml:
[package]
name = "mcp-my-tools"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "mcp-my-tools"
path = "src/main.rs"
[dependencies]
systemprompt-mcp = { path = "../../../core/crates/domain/mcp" }
rmcp = "0.14"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
Step 1: Define Tools
Create src/tools.rs:
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct WeatherParams {
pub location: String,
pub units: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct WeatherResult {
pub location: String,
pub temperature: f64,
pub conditions: String,
pub humidity: u8,
}
pub async fn get_weather(params: WeatherParams) -> Result<WeatherResult, String> {
// In production, call a real weather API
Ok(WeatherResult {
location: params.location,
temperature: 72.0,
conditions: "Sunny".into(),
humidity: 45,
})
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchParams {
pub query: String,
pub limit: Option<u32>,
}
#[derive(Debug, Serialize)]
pub struct SearchResult {
pub results: Vec<SearchItem>,
}
#[derive(Debug, Serialize)]
pub struct SearchItem {
pub title: String,
pub url: String,
pub snippet: String,
}
pub async fn search_web(params: SearchParams) -> Result<SearchResult, String> {
// In production, call a search API
let limit = params.limit.unwrap_or(5);
Ok(SearchResult {
results: vec![
SearchItem {
title: format!("Result for: {}", params.query),
url: "https://example.com".into(),
snippet: "Example search result...".into(),
}
],
})
}
Step 2: Create the Server
Create src/main.rs:
mod tools;
use rmcp::{
handler::server::wrapper::Json,
model::{Tool, ServerCapabilities, ServerInfo},
protocol::JsonRpcMessage,
service::server::{McpServer, McpServerHandler},
};
use serde_json::Value;
use std::sync::Arc;
struct MyToolsServer;
impl McpServerHandler for MyToolsServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
name: "my-tools".into(),
version: env!("CARGO_PKG_VERSION").into(),
}
}
fn get_capabilities(&self) -> ServerCapabilities {
ServerCapabilities {
tools: Some(Default::default()),
..Default::default()
}
}
async fn list_tools(&self) -> Vec<Tool> {
vec![
Tool {
name: "get_weather".into(),
description: Some("Get current weather for a location".into()),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name or coordinates"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"default": "fahrenheit"
}
},
"required": ["location"]
}),
},
Tool {
name: "search_web".into(),
description: Some("Search the web for information".into()),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
"limit": {
"type": "integer",
"description": "Max results",
"default": 5
}
},
"required": ["query"]
}),
},
]
}
async fn call_tool(&self, name: &str, args: Value) -> Result<Value, String> {
match name {
"get_weather" => {
let params: tools::WeatherParams =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let result = tools::get_weather(params).await?;
Ok(serde_json::to_value(result).unwrap())
}
"search_web" => {
let params: tools::SearchParams =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let result = tools::search_web(params).await?;
Ok(serde_json::to_value(result).unwrap())
}
_ => Err(format!("Unknown tool: {}", name)),
}
}
}
#[tokio::main]
async fn main() {
let server = McpServer::new(Arc::new(MyToolsServer));
// Run with stdio transport for local development
server.run_stdio().await.unwrap();
}
Step 3: Define Server Configuration
Create services/mcp/my-tools.yaml:
name: my-tools
description: Weather and search tools
binary: mcp-my-tools
transport:
type: streamable-http
endpoint: "/api/v1/mcp/my-tools/mcp"
oauth:
required: true
scopes: ["tools:read"]
audience: ["systemprompt"]
tools:
- name: get_weather
description: Get current weather for a location
parameters:
location:
type: string
required: true
description: City name or coordinates
units:
type: string
enum: ["celsius", "fahrenheit"]
default: "fahrenheit"
- name: search_web
description: Search the web for information
parameters:
query:
type: string
required: true
description: Search query
limit:
type: integer
default: 5
description: Maximum number of results
Step 4: Build and Register
Build the MCP server:
just build-mcp
# or
cargo build --release -p mcp-my-tools
Sync to database:
systemprompt cloud sync local mcp --direction to-db -y
Start SystemPrompt:
just start
Step 5: Test
Check the MCP registry:
curl http://localhost:3000/api/v1/mcp/registry
Test tool listing:
curl -X POST http://localhost:3000/api/v1/mcp/my-tools/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'
Test tool execution:
curl -X POST http://localhost:3000/api/v1/mcp/my-tools/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {"location": "San Francisco"}
},
"id": 2
}'
Step 6: Connect Claude Desktop
Add to Claude Desktop configuration:
{
"mcpServers": {
"my-tools": {
"url": "http://localhost:3000/api/v1/mcp/my-tools/mcp",
"transport": "streamable-http"
}
}
}
For authenticated access:
{
"mcpServers": {
"my-tools": {
"url": "https://your-tenant.systemprompt.io/api/v1/mcp/my-tools/mcp",
"transport": "streamable-http",
"headers": {
"Authorization": "Bearer YOUR_ACCESS_TOKEN"
}
}
}
}
Adding Resources
MCP servers can also provide resources (files, content):
impl McpServerHandler for MyToolsServer {
async fn list_resources(&self) -> Vec<Resource> {
vec![
Resource {
uri: "mytools://config".into(),
name: "Configuration".into(),
mime_type: Some("application/json".into()),
}
]
}
async fn read_resource(&self, uri: &str) -> Result<ResourceContent, String> {
match uri {
"mytools://config" => Ok(ResourceContent {
uri: uri.into(),
mime_type: Some("application/json".into()),
text: Some(serde_json::json!({"version": "1.0"}).to_string()),
blob: None,
}),
_ => Err(format!("Unknown resource: {}", uri)),
}
}
}
Next Steps
- Add OAuth2 scopes for fine-grained permissions
- Implement SSE transport for real-time updates
- Add logging and error handling
- See systemprompt-mcp for advanced features