Skip to main content

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

  1. Client calls initialize and receives a session ID.
  2. Client sends notifications/initialized.
  3. Client calls tools/list and gets JSON schemas from your Rust input/output types.
  4. 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 TypeMCP Behavior
ForgeError::Validation, ForgeError::InvalidArgumentTool result with isError: true
ForgeError::UnauthorizedJSON-RPC error -32001
ForgeError::ForbiddenJSON-RPC error -32003
Other errorsJSON-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:

MethodDescription
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.authAuth 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.