Skip to main content

Coming from TypeScript / Python

If you've built MCP servers with the TypeScript SDK or Python SDK, this page maps familiar patterns to their Scala equivalents.

For Scala syntax basics, see Key Concepts.

Defining a Tool

TypeScript

server.tool(
"greet",
"Greet someone",
{
name: z.string().describe("Name to greet"),
excited: z.boolean().optional().describe("Add exclamation marks")
},
async ({ name, excited }) => ({
content: [{ type: "text", text: `Hello, ${name}${excited ? "!!!" : "!"}` }]
})
);

Python

from pydantic import Field

@server.tool("greet", "Greet someone")
async def greet(
name: str = Field(description="Name to greet"),
excited: bool = Field(default=False, description="Add exclamation marks")
) -> list[TextContent]:
mark = "!!!" if excited else "!"
return [TextContent(type="text", text=f"Hello, {name}{mark}")]

Scala

type GreetInput = (name: String, excited: Option[Boolean])
given InputDef[GreetInput] = InputDef[GreetInput](
name = InputField[String]("Name to greet"),
excited = InputField[Option[Boolean]]("Add exclamation marks")
)

val greetTool = ToolDef.unstructured[IO, GreetInput](
name = "greet",
description = Some("Greet someone")
) { (input, ctx) =>
val mark = if input.excited.getOrElse(false) then "!!!" else "!"
IO.pure(List(Content.Text(s"Hello, ${input.name}$mark")))
}

Input Schema

TypeScript (Zod)PythonScala
z.string().describe("...")str = Field(description="...")InputField[String]("...")
z.number().describe("...")float = Field(description="...")InputField[Double]("...")
z.boolean().describe("...")bool = Field(description="...")InputField[Boolean]("...")
z.string().optional()str | None = NoneInputField[Option[String]]("...")
z.object({...})Pydantic BaseModelInputDef[MyType](...)

All three SDKs generate JSON Schema from these definitions. The key difference in Scala: the schema definition (InputDef) is separate from the type definition (named tuple or case class). The given keyword makes it available to ToolDef automatically — you don't pass it explicitly.

Server Setup

TypeScript

const server = new McpServer({ name: "my-server", version: "1.0.0" });
server.tool("greet", ...);

const transport = new StdioServerTransport();
await server.connect(transport);

Python

server = Server("my-server")

@server.tool()
async def greet(...): ...

async with stdio_server() as (read, write):
await server.run(read, write, InitializationOptions(...))

Scala

object MyServer extends IOApp.Simple {
def run: IO[Unit] =
(for {
server <- McpServer[IO](
info = Implementation("my-server", "1.0.0"),
tools = List(greetTool)
)
transport <- StdioTransport[IO]()
_ <- server.serve(transport)
} yield ()).useForever
}

The main structural difference: in Scala, tools are passed to the server at creation rather than registered via method calls. The for/yield block sequences the setup, and Resource handles cleanup automatically.

Key Differences

ConceptTypeScript / PythonScala T
Async modelasync/awaitIO[A] with for/yield
Schema definitionZod / type hintsInputDef + InputField
Schema wiringInline argumentgiven/using (implicit)
Tool registrationserver.tool(...)Passed to McpServer(tools = ...)
Optional fields.optional() / None defaultOption[A]
Return type{ content: [...] }IO[List[Content]]
Cleanup / lifecycleManual / context managersResource (automatic)