systemprompt-mcp

Model Context Protocol server registry and tool execution

Model Context Protocol implementation providing server registry, tool execution, and HTTP-native transports.

Overview

The MCP crate implements the Model Context Protocol, enabling AI clients like Claude Desktop and ChatGPT to interact with SystemPrompt's tool servers. It handles server lifecycle, tool discovery, and authenticated execution.

Layer Position

Domain Layer
├── systemprompt-users
├── systemprompt-oauth
├── systemprompt-mcp  ← You are here
├── systemprompt-ai
└── ...

Depends on: systemprompt-database, systemprompt-security, systemprompt-events Used by: systemprompt-api, systemprompt-agent

Key Features

  • HTTP-native transports - Streamable HTTP, SSE
  • OAuth2 authentication - Per-tool permission scopes
  • Server registry - Dynamic server management
  • Tool execution - Type-safe tool calls
  • Resource providers - Content and file resources

Configuration

Define MCP servers in services/mcp/:

# services/mcp/content.yaml
name: content
description: Content management tools
binary: mcp-content

transport:
  type: streamable-http
  endpoint: "/api/v1/mcp/content/mcp"

oauth:
  required: true
  scopes: ["content:read", "content:write"]
  audience: ["systemprompt"]

tools:
  - name: search_content
    description: Search for content by query
    parameters:
      query:
        type: string
        required: true
      limit:
        type: integer
        default: 10

  - name: create_content
    description: Create new content
    parameters:
      title:
        type: string
        required: true
      body:
        type: string
        required: true

Public API

McpRegistry

Server registry for managing MCP servers:

use systemprompt_mcp::{McpRegistry, McpServer};

// Get registry from context
let registry = ctx.mcp_registry.clone();

// List all servers
let servers = registry.list_servers().await?;

// Get specific server
let server = registry.get_server("content").await?;

// Register new server
registry.register(McpServer {
    name: "custom".into(),
    endpoint: "/api/v1/mcp/custom/mcp".into(),
    // ...
}).await?;

Tool Execution

use systemprompt_mcp::{ToolCall, ToolResult};

// Execute a tool
let result = registry.execute_tool(ToolCall {
    server: "content".into(),
    tool: "search_content".into(),
    arguments: serde_json::json!({
        "query": "rust tutorial",
        "limit": 5
    }),
}).await?;

match result {
    ToolResult::Success(value) => println!("Result: {}", value),
    ToolResult::Error(err) => println!("Error: {}", err),
}

Resource Access

use systemprompt_mcp::ResourceProvider;

// List resources
let resources = provider.list_resources().await?;

// Read resource
let content = provider.read_resource("content://posts/123").await?;

HTTP Endpoints

The MCP crate exposes these endpoints:

Endpoint Method Description
/api/v1/mcp/registry GET List all MCP servers
/api/v1/mcp/{server}/mcp POST MCP message endpoint
/api/v1/mcp/{server}/sse GET SSE transport

Claude Desktop Integration

Configure Claude Desktop to use SystemPrompt MCP servers:

{
  "mcpServers": {
    "content": {
      "url": "https://your-tenant.systemprompt.io/api/v1/mcp/content/mcp",
      "transport": "streamable-http",
      "headers": {
        "Authorization": "Bearer YOUR_TOKEN"
      }
    }
  }
}

Building MCP Servers

Create a new MCP server in extensions/mcp/:

use systemprompt_mcp::{McpServer, Tool, ToolHandler};

pub struct ContentServer;

impl McpServer for ContentServer {
    fn name(&self) -> &str { "content" }

    fn tools(&self) -> Vec<Tool> {
        vec![
            Tool::new("search_content")
                .description("Search for content")
                .parameter("query", ParameterType::String, true),
        ]
    }
}

#[async_trait]
impl ToolHandler for ContentServer {
    async fn handle(&self, call: ToolCall) -> Result<ToolResult> {
        match call.tool.as_str() {
            "search_content" => {
                let query = call.get_string("query")?;
                let results = search(&query).await?;
                Ok(ToolResult::Success(serde_json::to_value(results)?))
            }
            _ => Err(McpError::UnknownTool),
        }
    }
}