MCP Extensions
Build standalone MCP server extensions that expose tools for AI agents via the Model Context Protocol.
On this page
MCP (Model Context Protocol) extensions are standalone binaries that expose tools for AI agents. Unlike library extensions that compile into the main binary, MCP servers run as separate processes. They listen on TCP ports and serve tool requests via the MCP protocol, enabling AI clients like Claude to execute operations in your SystemPrompt environment.
Standalone Binary Pattern
MCP extensions are not library extensions. They do not implement the Extension trait or register with the runtime. Instead, they are independent executables with their own entry point, their own database connection, and their own lifecycle.
This separation provides important benefits:
- Independent scaling - Run multiple MCP server instances on different machines
- Process isolation - MCP server crashes do not affect the main runtime
- Resource control - Allocate specific CPU and memory limits
- Separate deployment - Update MCP servers without redeploying the main binary
For CLI tools that agents invoke via subprocess rather than the MCP protocol, see CLI Extensions.
Project Structure
extensions/mcp/systemprompt/
├── Cargo.toml
└── src/
├── main.rs # Entry point with bootstrap
├── lib.rs # Server implementation
├── server.rs # MCP protocol handler
├── tools.rs # Tool definitions
└── artifacts.rs # Artifact storage
Entry Point
The MCP server entry point bootstraps from SystemPrompt's configuration system and starts an HTTP server:
use anyhow::{Context, Result};
use std::{env, sync::Arc};
use systemprompt::identifiers::McpServerId;
use systemprompt::models::{Config, ProfileBootstrap, SecretsBootstrap};
use systemprompt::system::AppContext;
use tokio::net::TcpListener;
const DEFAULT_SERVICE_ID: &str = "systemprompt";
const DEFAULT_PORT: u16 = 5010;
#[tokio::main]
async fn main() -> Result<()> {
// Bootstrap from profile and secrets
ProfileBootstrap::init().context("Failed to initialize profile")?;
SecretsBootstrap::init().context("Failed to initialize secrets")?;
Config::init().context("Failed to initialize configuration")?;
// Create application context with database access
let ctx = Arc::new(
AppContext::new()
.await
.context("Failed to initialize application context")?,
);
systemprompt::logging::init_logging(ctx.db_pool().clone());
// Get service ID from environment or use default
let service_id = McpServerId::from_env().unwrap_or_else(|_| {
tracing::warn!("MCP_SERVICE_ID not set, using default: {DEFAULT_SERVICE_ID}");
McpServerId::new(DEFAULT_SERVICE_ID)
});
let port = env::var("MCP_PORT")
.ok()
.and_then(|p| p.parse::<u16>().ok())
.unwrap_or(DEFAULT_PORT);
// Create MCP server and router
let server = SystempromptServer::new(ctx.db_pool().clone(), service_id.clone());
let router = systemprompt::mcp::create_router(server, ctx.db_pool());
let addr = format!("0.0.0.0:{port}");
let listener = TcpListener::bind(&addr).await?;
tracing::info!(
service_id = %service_id,
addr = %addr,
"SystemPrompt MCP server listening"
);
axum::serve(listener, router).await?;
Ok(())
}
Key bootstrap steps:
- ProfileBootstrap - Loads the active profile configuration
- SecretsBootstrap - Loads secrets from environment or files
- Config::init - Initializes the configuration system
- AppContext - Creates database pool and shared resources
This allows MCP servers to share the same configuration and database as the main runtime.
Server Implementation
The MCP server implements the protocol handler and registers tools:
use rmcp::{Server, ServerHandler, Tool, ToolResult};
use sqlx::PgPool;
use std::sync::Arc;
pub struct SystempromptServer {
pool: Arc<PgPool>,
service_id: McpServerId,
}
impl SystempromptServer {
pub fn new(pool: Arc<PgPool>, service_id: McpServerId) -> Self {
Self { pool, service_id }
}
}
#[async_trait]
impl ServerHandler for SystempromptServer {
async fn list_tools(&self) -> Vec<Tool> {
vec![
Tool::new("execute_cli", "Execute a SystemPrompt CLI command"),
Tool::new("query_content", "Query content from the database"),
Tool::new("list_agents", "List configured agents"),
]
}
async fn call_tool(&self, name: &str, args: Value) -> ToolResult {
match name {
"execute_cli" => self.execute_cli(args).await,
"query_content" => self.query_content(args).await,
"list_agents" => self.list_agents(args).await,
_ => ToolResult::error(format!("Unknown tool: {}", name)),
}
}
}
Tool Definitions
Each tool has a name, description, and input schema:
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Deserialize)]
struct ExecuteCliInput {
command: String,
args: Vec<String>,
}
impl SystempromptServer {
async fn execute_cli(&self, args: Value) -> ToolResult {
let input: ExecuteCliInput = serde_json::from_value(args)?;
let output = std::process::Command::new("systemprompt")
.arg(&input.command)
.args(&input.args)
.output()?;
if output.status.success() {
ToolResult::success(String::from_utf8_lossy(&output.stdout))
} else {
ToolResult::error(String::from_utf8_lossy(&output.stderr))
}
}
fn execute_cli_schema() -> Value {
json!({
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "CLI command to execute"
},
"args": {
"type": "array",
"items": { "type": "string" },
"description": "Command arguments"
}
},
"required": ["command"]
})
}
}
Configuration
Register MCP servers in services/mcp/:
# services/mcp/systemprompt.yaml
mcp_servers:
systemprompt:
binary: "systemprompt-mcp-agent"
port: 5010
endpoint: "http://localhost:8080/api/v1/mcp/systemprompt/mcp"
enabled: true
oauth:
required: true
scopes: ["admin"]
Cargo Configuration
[package]
name = "systemprompt-mcp-agent"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "systemprompt-mcp-agent"
path = "src/main.rs"
[dependencies]
systemprompt = { workspace = true }
rmcp = { workspace = true }
axum = { workspace = true }
tokio = { workspace = true, features = ["full"] }
sqlx = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
tracing = { workspace = true }
Building
# Build MCP server
cargo build --release -p systemprompt-mcp-agent
# Or build all MCP servers
systemprompt build mcp --release
Testing
# Test server connectivity
systemprompt plugins mcp test systemprompt
# List available tools
systemprompt plugins mcp tools systemprompt
# Start server manually
./target/release/systemprompt-mcp-agent
Claude Desktop Integration
Add to Claude Desktop configuration:
{
"mcpServers": {
"systemprompt": {
"url": "http://localhost:8080/api/v1/mcp/systemprompt/mcp",
"transport": "streamable-http"
}
}
}