An MCP server that executes Scala 3 code in a sandboxed environment via an embedded REPL. It enforces security through a capability-based system using Scala 3's experimental capture checking, preventing unauthorized access to the file system, processes, and network.
Supports both stateless one-shot execution and stateful sessions that persist definitions across calls.
sbt assembly
java -jar target/scala-3.8.2-RC1/SafeExecMCP-assembly-0.1.0-SNAPSHOT.jarTo enable execution logging:
java -jar target/scala-3.8.2-RC1/SafeExecMCP-assembly-0.1.0-SNAPSHOT.jar --record ./logTo use a JSON config file:
java -jar target/scala-3.8.2-RC1/SafeExecMCP-assembly-0.1.0-SNAPSHOT.jar --config config.jsonClaude Desktop (~/.config/claude/claude_desktop_config.json):
{
"mcpServers": {
"scala-exec": {
"command": "java",
"args": ["-jar", "/path/to/SafeExecMCP-assembly-0.1.0-SNAPSHOT.jar"]
}
}
}Using sbt directly (for development)
{
"mcpServers": {
"scala-exec": {
"command": "sbt",
"args": ["--error", "run"],
"cwd": "/path/to/SafeExecMCP"
}
}
}| Tool | Parameters | Description |
|---|---|---|
execute_scala |
code |
Execute a Scala snippet in a fresh REPL (stateless) |
create_repl_session |
— | Create a persistent REPL session, returns session_id |
execute_in_session |
session_id, code |
Execute code in an existing session (stateful) |
list_sessions |
— | List active session IDs |
delete_repl_session |
session_id |
Delete a session |
show_interface |
— | Show the full capability API reference |
1. create_repl_session → session_id: "abc-123"
2. execute_in_session(code: "val x = 42") → x: Int = 42
3. execute_in_session(code: "x * 2") → val res0: Int = 84
4. delete_repl_session(session_id: "abc-123")
All user code is validated before execution. Direct use of java.io, java.nio, java.net, ProcessBuilder, Runtime.getRuntime, reflection APIs, and other unsafe APIs is rejected. Instead, users access these resources through a capability-based API that is automatically injected into every REPL session.
The API exposes three capability request methods, each scoping access to a block:
// File system — scoped to a root directory
requestFileSystem("/tmp/work") {
val f = access("data.txt")
f.write("hello")
val lines = f.readLines()
grep("data.txt", "hello")
find(".", "*.txt")
}
// Process execution — scoped to an allowlist of commands
requestExecPermission(Set("ls", "cat")) {
val result = exec("ls", List("-la"))
println(result.stdout)
}
// Network — scoped to an allowlist of hosts
requestNetwork(Set("api.example.com")) {
val body = httpGet("https://api.example.com/data")
httpPost("https://api.example.com/submit", """{"key":"value"}""")
}Capabilities cannot escape their scoped block — this is enforced at compile time by Scala 3's capture checker.
A secondary LLM is available through the chat method — no capability scope required. Safety comes from the Classified type system: chat(String): String for regular data, chat(Classified[String]): Classified[String] for sensitive data.
// Regular chat
val answer = chat("What is 2 + 2?")
// Classified chat — input and output stay wrapped
requestFileSystem("/secrets") {
val secret = readClassified("/secrets/key.txt")
val result = chat(secret.map(s => s"Summarize: $s"))
// result is Classified[String] — cannot be printed or leaked
}Configure via CLI flags (--llm-base-url, --llm-api-key, --llm-model) or a JSON config file (--config). Any OpenAI-compatible API is supported.
Code is checked against 40+ forbidden patterns before execution, covering:
- File I/O bypass (
java.io.*,java.nio.*,scala.io.*) - Process bypass (
ProcessBuilder,Runtime.getRuntime,scala.sys.process) - Network bypass (
java.net.*,javax.net.*,HttpClient) - Reflection (
getDeclaredMethod,setAccessible,Class.forName) - JVM internals (
sun.misc.*,jdk.internal.*) - Capture checking escape (
caps.unsafe,unsafeAssumePure,.asInstanceOf) - System control (
System.exit,System.setProperty,new Thread) - Class loading (
ClassLoader,URLClassLoader)
The server can be configured via CLI flags or a JSON config file.
| Flag | Description |
|---|---|
-r/--record <dir> |
Log every execution to disk |
-s/--strict |
Block file ops (cat, ls, rm, etc.) through exec |
--classified-paths <paths> |
Comma-separated classified (protected) paths |
-c/--config <path> |
JSON config file (flags after --config override file values) |
--llm-base-url <url> |
LLM API base URL |
--llm-api-key <key> |
LLM API key |
--llm-model <name> |
LLM model name |
{
"recordPath": "/tmp/recordings",
"strictMode": true,
"classifiedPaths": ["/home/user/secret"],
"llm": {
"baseUrl": "https://api.openai.com",
"apiKey": "sk-...",
"model": "gpt-4o-mini"
}
}sbt clean # Clean build artifacts
sbt compile # Compile
sbt test # Run all tests
sbt "testOnly *McpServerSuite" # Run a single suite
sbt run # Run the server locally
sbt "run --record ./log" # Run with logging enabled
sbt "run --config config.json" # Run with JSON config
sbt assembly # Build fat JAR- JDK 17+
- sbt 1.12+
MIT