Expose MCP Tools
Expose explicit MCP tools from Forge over Streamable HTTP (/_api/mcp) with built-in auth, role checks, rate limits, and timeout controls.
The Code
use forge::prelude::*;
#[derive(Debug, serde::Serialize, JsonSchema)]
pub struct ExportProjectOutput {
pub job_id: uuid::Uuid,
pub queued: bool,
}
#[forge::mcp_tool(
name = "export_project",
title = "Export Project",
description = "Queue an async project export job",
timeout = 15,
rate_limit(requests = 30, per = "1m", key = "user"),
idempotent
)]
pub async fn export_project(
ctx: &McpToolContext,
#[schemars(description = "Project UUID to export")]
project_id: uuid::Uuid,
) -> Result<ExportProjectOutput> {
let user_id = ctx.require_user_id()?;
let job_id = ctx
.dispatch_job(
"export_project",
serde_json::json!({
"user_id": user_id,
"project_id": project_id,
"format": "json"
}),
)
.await?;
Ok(ExportProjectOutput {
job_id,
queued: true,
})
}
Parameter Metadata (Descriptions, Enums, Validation)
MCP clients read parameter metadata from inputSchema (JSON Schema). Forge generates this from your Rust input types via schemars.
use forge::prelude::*;
#[derive(Debug, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExportFormat {
Json,
Csv,
}
#[derive(Debug, serde::Deserialize, JsonSchema)]
pub struct ExportProjectInput {
#[schemars(description = "Project UUID to export")]
pub project_id: uuid::Uuid,
#[schemars(description = "Export output format")]
pub format: ExportFormat,
#[schemars(description = "Optional target bucket", length(min = 3, max = 63))]
pub bucket: Option<String>,
}
For inline parameters (when Forge auto-generates the tool args struct), parameter-level #[schemars(...)] and #[serde(...)] attributes are also propagated:
#[forge::mcp_tool(name = "search_projects")]
pub async fn search_projects(
ctx: &McpToolContext,
#[schemars(description = "Query string", length(min = 2, max = 120))]
query: String,
#[serde(default)]
limit: Option<u32>,
) -> Result<SearchProjectsOutput> {
// ...
}
Registration
Register tools explicitly on the runtime builder:
let mut builder = Forge::builder();
builder.register_mcp_tool::<functions::ExportProjectMcpTool>();
Only registered tools are exposed to MCP clients.
Configuration
Enable the MCP endpoint in forge.toml:
[mcp]
enabled = true
path = "/mcp"
session_ttl_secs = 3600
allowed_origins = ["https://your-app.example"]
require_protocol_version_header = true
With default routing, the MCP URL is:
/_api/mcp
What Happens
- Client calls
initializeand receives a session ID. - Client sends
notifications/initialized. - Client calls
tools/listand gets JSON schemas from your Rust input/output types. - Client calls
tools/call; Forge applies auth/role/rate/timeout policy before invoking your tool.
Tool Annotations
Annotation hints tell MCP clients about tool behavior. Clients may use these to optimize calls or warn users.
#[forge::mcp_tool(
name = "delete_project",
destructive, // Modifies external state irreversibly
)]
#[forge::mcp_tool(
name = "list_projects",
read_only, // No side effects, safe to call speculatively
)]
#[forge::mcp_tool(
name = "create_export",
idempotent, // Repeated calls with same args produce same result
)]
#[forge::mcp_tool(
name = "web_search",
open_world, // Interacts with external entities beyond its dataset
)]
These map to annotations in the tools/list response per the MCP specification.
Input Struct Convention
When a tool takes a single struct parameter named *Args or *Input, the macro uses it directly as the tool schema:
#[derive(Debug, serde::Deserialize, JsonSchema)]
pub struct SearchArgs {
#[schemars(description = "Search query")]
pub query: String,
#[schemars(description = "Max results")]
pub limit: Option<u32>,
}
#[forge::mcp_tool(name = "search")]
pub async fn search(ctx: &McpToolContext, args: SearchArgs) -> Result<Vec<SearchResult>> {
// args.query, args.limit available directly
}
For inline parameters (multiple args), Forge auto-generates the schema struct.
Error Handling
Forge maps errors to MCP protocol responses:
| Error Type | MCP Behavior |
|---|---|
ForgeError::Validation, ForgeError::InvalidArgument | Tool result with isError: true |
ForgeError::Unauthorized | JSON-RPC error -32001 |
ForgeError::Forbidden | JSON-RPC error -32003 |
| Other errors | JSON-RPC error -32603 |
Validation errors return as tool results so LLM clients can read and correct their input. Auth errors return as protocol errors since they indicate a session problem.
Context Methods
McpToolContext provides:
| Method | Description |
|---|---|
ctx.db() | Database pool |
ctx.db_conn() | Database connection as DbConn for shared helpers |
ctx.require_user_id()? | Authenticated user UUID |
ctx.require_subject()? | Auth subject string (Firebase/Auth0/Clerk) |
ctx.dispatch_job(name, args) | Enqueue a background job |
ctx.start_workflow(name, input) | Start a workflow |
ctx.env("KEY") | Read environment variable |
ctx.env_require("KEY")? | Require environment variable |
ctx.auth | Auth context (roles, claims) |
Protocol
Forge implements MCP protocol version 2025-11-25 over Streamable HTTP. Tools are separate from #[forge::query] and #[forge::mutation] handlers.