fastMCP4J

Framework MCP serveur Java rapide et léger - Construisez des serveurs Model Context Protocol avec un minimum de code standard et une compatibilité totale avec le SDK TypeScript

Documentation

FastMCP4J

Maven Central Java CI License Tests MCP Marketplace

Annotate a Java class. Ship an MCP server.

Tools, resources, prompts, memory — and a sandboxed bash terminal that can't touch your machine. No boilerplate, no containers, no 50-jar framework.

AI Agents → Share this skill with Claude for code generation

An AI agent driving the sandboxed bash tool: virtual identity, read-only project mounts, network denied, destructive commands blocked — writes stay in the sandbox.

FastMCP4J is an annotation-driven SDK for the Model Context Protocol (spec 2.0.1) on Java 17+. One annotation turns a class into an MCP server; one more gives your AI agent a real terminal — a bash sandbox where the host is unreachable by construction. Cold start in under 500 ms, twelve dependencies, everything testable in-process.

Status: beta (v0.5.0-beta) — API stable, 235 tests passing, published to Maven Central.


Quick start (2 minutes)

Maven

<dependency>
    <groupId>io.github.terseprompts.fastmcp</groupId>
    <artifactId>fastmcp-java</artifactId>
    <version>0.5.0-beta</version>
</dependency>

Gradle

implementation 'io.github.terseprompts.fastmcp:fastmcp-java:0.5.0-beta'

That's the whole install story: Java 17+, one artifact on Maven Central, MCP Java SDK 2.0.1 underneath. No annotation processor, no codegen step, no container runtime.

Create your server

@McpServer(name = "Assistant", version = "1.0")
public class MyAssistant {

    @McpTool(description = "Summarize text")
    public String summarize(@McpParam(description = "Text") String text) {
        return "Summary: " + text.substring(0, Math.min(100, text.length()));
    }

    public static void main(String[] args) {
        FastMCP.server(MyAssistant.class)
            .stdio()           // or .sse() or .streamable()
            .run();
    }
}
mvn exec:java -Dexec.mainClass="com.example.MyAssistant"

That's it. Your MCP server is running.

Working example: EchoServer.java


Why this exists

Every Java team wiring AI agents to tools faces the same choice, and every option hurts:

Raw MCP SDKSpring AI / LangChain4jFastMCP4J
Lines per tool35+varies, plus framework glue~8
Dependencies1 + your patience30–50+ jars12 jars
Startupfastframework-sized<500 ms, ~64 MB
Sandboxed bashroll your ownnot included✅ built-in (bashkit4j)
Built-in memory / todo / planner / file toolsnono✅ one annotation each
Lock-innoneframeworknone — MCP in, MCP out

Before: a day of JSON-schema plumbing per tool, and shell access means a rogue prompt is a node down. After: annotations on Tuesday, agents in production Wednesday — and the terminal they use can't touch your disk.


The API tour

1 · Make a tool — sync or async

@McpTool(description = "Add two numbers")
public int add(int a, int b) {
    return a + b;
}

@McpTool(description = "Process data")
@McpAsync  // ← just add this; return Mono<?>
public Mono<String> process(@McpContext Context ctx, String input) {
    return Mono.fromCallable(() -> {
        ctx.reportProgress(50, "Processing...");
        return slowOperation(input);
    });
}

2 · Built-in brains — one annotation each

@McpServer(name = "MyServer", version = "1.0")
@McpMemory     // AI remembers things across sessions
@McpTodo       // AI manages tasks
@McpPlanner    // AI breaks work into plans
@McpFileRead   // AI reads your files
@McpFileWrite  // AI writes files
public class MyServer {
    // complete tool sets enabled, zero implementation
}
AnnotationTools you get
@McpMemorylist, read, create, replace, insert, delete, rename
@McpTodoadd, list, updateStatus, updateTask, delete, clearCompleted
@McpPlannercreatePlan, listPlans, getPlan, addTask, addSubtask
@McpFileReadreadLines, readFile, grep, getStats
@McpFileWritewriteFile, appendFile, writeLines, deleteFile, createDirectory

3 · Sandboxed bash — give the agent a terminal, not your machine

@McpServer(name = "Reviewer", version = "1.0")
@McpBash(
    allowMountsUnder = "C:/dev",           // opt in: all it may ever see
    mounts = {"/project=C:/dev/my-app"},   // mount the project — read-only
    timeout = 30, maxCommands = 10_000     // bounds runaway scripts
)
public class Reviewer { }

Scripts run in a bashkit4j in-memory sandbox: POSIX-style bash with 160+ commands re-implemented natively, a virtual filesystem, network denied by default. No real bash is ever spawned. Then the agent calls the bash tool:

grep -rn TODO /project/src | head -5      # real files, zero risk
echo "findings..." > /notes.md            # writes stay in the sandbox
Host shell (ProcessBuilder, Docker)Sandbox mode
Real bash on the host✅ runs — full attack surface❌ never — bash re-implemented natively
Host filesystem visible✅ all of it❌ invisible until you mount, read-only by default
OS processes per command✅ one per call❌ zero — in-process
Network access✅ open❌ denied by default
One rogue scriptnode downsandbox reset

State persists across calls (cwd, env, files) — multi-step agent workflows work. Mounts are enforced inside the native library: canonicalized, symlink-safe, and impossible without allowMountsUnder. Need the real shell for trusted automation? mode = BashMode.HOST keeps the legacy tool with its path guardrails.

Sandbox mode requires the optional io.github.terseprompts:bashkit4j:0.2.0 dependency — native libs for Windows/Linux/macOS (x86-64 + ARM64) are bundled and auto-detected.

🔭 Future scope — a third mode, BashMode.DOCKER. The isolation ladder gets one more rung: HOST = the full computer (trusted use) → SANDBOX = a virtual computer, no real code execution (default) → DOCKER = real bash inside a per-session container jail. The sketch: one container per MCP session, lazy-created and hardened at run (--network none --memory 512m --cpus 1 --pids-limit 64 --security-opt no-new-privileges --read-only --tmpfs /tmp); the command passed as a single argv element (no host-shell interpolation → no injection surface); the timeout enforced inside the container so the wall-clock survives a server crash; named exit codes ([exit 124: timed out], [exit 137: OOM]), capped output, idle-TTL eviction with a reaper backstop — containers stay stateless, durable state lives only in the mounted worktree. Config stays env-only (FASTMCP_DOCKER_IMAGE, timeout). Not started — see Roadmap; help wanted.

4 · Pick a transport

FastMCP.server(MyServer.class)
    .stdio()       // CLI tools, local agents
    .sse()         // web clients, long-lived connections
    .streamable()  // bidirectional streaming (recommended)
    .run();
FastMCP.server(MyServer.class)
    .port(3000)                              // HTTP port
    .requestTimeout(Duration.ofMinutes(5))   // request timeout
    .keepAliveSeconds(30)                    // keep-alive interval
    .capabilities(c -> c
        .tools(true)
        .resources(true, true)
        .prompts(true))
    .run();

5 · Resources & prompts

@McpResource(uri = "config://settings")
public String getSettings() {
    return "{\"theme\": \"dark\"}";
}

@McpPrompt(name = "code-review")
public String codeReviewPrompt(@McpParam(description = "Code to review") String code) {
    return "Review this code:\n" + code;
}

6 · Hooks — before/after every tool call

// Run before ALL tools (*)
@McpPreHook(toolName = "*", order = 1)
void authenticate(Map<String, Object> args) {
    String token = (String) args.get("token");
    if (!isValid(token)) throw new SecurityException("Unauthorized");
}

// Run after a specific tool only
@McpPostHook(toolName = "calculate", order = 1)
void logResult(Map<String, Object> args, Object result) {
    System.out.println("Result: " + result);
}
  • toolName — target tool, or "*" for all (empty = inferred from method name)
  • order — execution priority, lower runs first (default 0)
  • Pre-hooks receive the arguments; post-hooks receive arguments + result

7 · Request context

@McpTool(description = "Read file with auth")
public String readFile(@McpContext Context context, String path) {
    context.info("Reading file: " + path);
    String auth = context.getRequestHeaders().get("Authorization");
    // ...
}

Context gives you getClientId(), getSessionId(), getToolName(), getRequestHeaders(), info/warning/error logging, reportProgress, listResources(), listPrompts().

8 · Organize at scale

// explicit modules
@McpServer(name = "MyServer", version = "1.0",
    modules = {StringTools.class, MathTools.class})

// or package scanning
@McpServer(name = "MyServer", version = "1.0",
    scanBasePackage = "com.example.tools")

9 · Icons & telemetry

@McpServer(
    name = "my-server",
    icons = {"data:image/svg+xml;base64,...:image/svg+xml:64x64:light"}
)
@McpTelemetry(enabled = true, exportConsole = true, sampleRate = 1.0)
public class MyServer { }

Telemetry collects tool invocation counters, duration histograms, and error rates — console or OpenTelemetry export.


Annotations reference

AnnotationTargetPurpose
@McpServerTYPEDefine your MCP server
@McpToolMETHODExpose as callable tool
@McpResourceMETHODExpose as resource
@McpPromptMETHODExpose as prompt template
@McpParamPARAMETERDescription, examples, constraints, defaults
@McpAsyncMETHODMake tool async (return Mono<?>)
@McpContextPARAMETERInject request context
@McpPreHook / @McpPostHookMETHODRun code before/after tool calls
@McpBashTYPEBash tool — sandboxed (default) or host shell via mode
@McpTelemetryTYPEMetrics and tracing
@McpMemory / @McpTodo / @McpPlannerTYPEBuilt-in tool sets
@McpFileRead / @McpFileWriteTYPEBuilt-in file tools

@McpParam advanced options:

@McpTool(description = "Create task")
public String createTask(
    @McpParam(
        description = "Task name",
        examples = {"backup", "sync"},
        constraints = "Cannot be empty",
        defaultValue = "default",
        required = false
    ) String taskName
) { return "Created: " + taskName; }

What the sandbox actually does (measured, not claimed)

FastMCP4J ships 235 tests (mvn test) — the sandbox suite runs against the real native library on Windows and Linux CI, including deliberate escape probes:

ProbeResult
ls / inside the sandboxvirtual root only — pom.xml, target, host paths absent
whoami / hostnameagent@sandbox — virtual identity, not your OS user
Write to a read-only mountfails; host file provably never appears
Read-write mountround-trips to the host through the Java API
cd + file across tool callspersists inside a server; sealed between servers
Script exceeding timeoutcaller gets TIMEOUT; sandbox replaced fresh for next call
4 concurrent tool callsall complete — calls serialize on the sandbox
Sandbox without bashkit4j on classpathclear startup error naming the dependency

Who it's for

  • AI/LLM engineers — expose Java services to Claude, Cursor, or any MCP client with annotation-level effort.
  • Teams shipping agent tools — memory, todo, planning, file access, and a sandboxed terminal out of the box; hooks and telemetry for production.
  • Security-conscious platforms — agent terminal access with the host unreachable by construction, not by prompt-engineering.
  • Existing Spring/DI codebases — drop-in server, no framework lock-in; your beans become tools with an annotation.

Requirements & performance

Just Java 17+ and Maven 3.8+. MCP spec 2.0.1 via the official Java SDK (mcp-core + mcp-json-jackson2).

  • Cold start: <500 ms
  • Tool invocation: <5 ms
  • Memory: ~64 MB
  • Purpose-built for MCP — not a general AI framework

CI/CD

BranchWhat runs
development (staging)Full test suite + MCP integration tests (STDIO / SSE / Streamable / Sandboxed Bash) on every push and PR
mainSame suite + publish to Maven Central (staged; manual approval in Sonatype Central)

Flow: feature branch → development → main (release).


Documentation


License

MIT © 2026


Less boilerplate. More shipping.

Get started • Examples • Docs

Made with ❤️ for the Java community