Module: tramai-mcp
One-liner: Exposes registered Tramai workflows as MCP (Model Context Protocol) tools —
list_workflows,run_workflow,resume_workflow,get_workflow_status— over stdio or SSE transport, so AI agents can discover and invoke workflows through the standard MCP contract. Module type:adapterSource files: 3 —TramaiMcpServer.kt,McpToolHandlers.kt,TramaiMcpAutoConfiguration.ktTest files: 1 —TramaiMcpServerTest.ktBuild:dev.tramai:tramai-mcp:0.3.1Depends on:tramai-server(required),tramai-structured(required),spring-boot-autoconfigure,spring-context, MCP Kotlin SDK, Ktor CIO
L1: Quick Start (30-second read)
What
tramai-mcp is a Spring Boot auto-configured MCP server adapter that reads the WorkflowRegistry and WorkflowController already present from tramai-server and exposes four MCP tools:
| Tool | Purpose |
|---|---|
list_workflows | Returns registered workflows with JSON Schema for input and output payloads |
run_workflow | Starts a workflow run from a JSON state object, returns workflow ID |
resume_workflow | Resumes a suspended workflow run by workflow ID |
get_workflow_status | Fetches current status, active step, history, result, and error state |
It does not reimplement workflow execution — it delegates every call through the existing WorkflowController and WorkflowRunStore from tramai-server. The module is an adapter layer, not a second orchestration runtime.
Why
AI coding tools, agent frameworks, and local CLIs speak MCP to discover capabilities and call tools. Without tramai-mcp, workflow invocation requires raw REST calls or direct Java API usage. With it:
- Agent-native discovery — any MCP client can call
tools/listand learn about all registered workflows, including their input/output JSON schemas - Uniform tool contract — workflow invocation looks like any other MCP tool call; agent frameworks don't need special-case REST client code
- Transport flexibility — stdio for local co-process agents (e.g., Claude Desktop, VS Code extensions), SSE for network-accessible MCP clients
- Error normalization — server-side exceptions are mapped to MCP error types (
not_found,conflict,invalid_request,internal_error) so agents see consistent failure shapes
When to use
Use this module when:
- A local coding tool or AI agent should discover workflows automatically
- Workflow invocation should look like normal MCP tool usage to the client
- You want workflow input/output contracts exposed as JSON Schema through MCP
- You already have tramai-server in your application
Don't use this module when:
- No MCP client will consume it (the REST API from tramai-server is sufficient)
- You don't need agent-native tool discovery
- You want to avoid the MCP SDK dependency
How to add
// build.gradle.kts
dependencies {
implementation(platform("dev.tramai:tramai-bom:0.3.1"))
implementation("dev.tramai:tramai-server") // required dependency
implementation("dev.tramai:tramai-mcp")
}
<!-- pom.xml -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.tramai</groupId>
<artifactId>tramai-bom</artifactId>
<version>0.3.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.tramai</groupId>
<artifactId>tramai-server</artifactId>
</dependency>
<dependency>
<groupId>dev.tramai</groupId>
<artifactId>tramai-mcp</artifactId>
</dependency>
</dependencies>
Configuration
The adapter activates automatically via Spring Boot auto-configuration when the workflow server beans (WorkflowRegistry, WorkflowRunStore, WorkflowController) are present. It binds under the tramai.mcp.* namespace:
tramai:
mcp:
stdio:
enabled: false # starts an MCP stdio session in-process
sse:
enabled: false # starts an embedded Ktor Server-Sent Events server
host: 127.0.0.1
port: 8091
path: /mcp
At least one transport must be enabled for the server to start. Auto-configuration uses a SmartLifecycle bean that checks these flags and calls startStdio() and/or startSse() accordingly.
Where to go next
- MCP Integration Guide — Setup walkthrough and transport options
- Workflow Server Guide — Using tramai-server REST API
- Configuration Reference — All
tramai.mcp.*properties - tramai-server — Server module this adapter depends on
- tramai-orchestration — Workflow definitions that get exposed as tools
L2: Usage Guide (5-minute read)
Activation model
tramai-mcp is a conditional auto-configuration. It activates only when all three server beans are in the context:
WorkflowRegistry— holds registered workflow definitionsWorkflowRunStore— persists workflow run stateWorkflowController— orchestrates run/resume/status operations
This means tramai-server must already be in the application. The MCP adapter layers on top of the existing server infrastructure without replacing or duplicating it.
Tool definitions
Each tool is defined as an MCP SDK Tool with a name, description, inputSchema, and outputSchema:
list_workflows — No input arguments. Returns an array of workflow descriptors, each with:
name— workflow namedefinitionVersion— semantic version stringinputSchema— JSON Schema for the workflow's initial state type, generated byJacksonStructuredOutputHandleroutputSchema— JSON Schema for the workflow's result type
run_workflow — Required arguments:
workflowName(string) — registered workflow namestate(object) — initial JSON state for the workflow run- Optional
idempotencyKey(string) — deduplication key
Returns workflowId, status, definitionVersion, and optional result.
resume_workflow — Required argument:
workflowId(string) — ID of a suspended workflow run- Optional
workflowName(string) — if omitted, resolved by scanning all registered workflows
Returns the same response shape as run_workflow.
get_workflow_status — Required argument:
workflowId(string) — ID of an existing workflow run- Optional
workflowName(string) — if omitted, resolved by scanning all registered workflows
Returns workflowId, status, definitionVersion, currentStep, history (array of step entries), optional result, and optional error.
Error mapping
The adapter normalizes server-side exceptions into MCP tool failures through McpToolHandlers.toolFailure():
| Server exception | MCP errorType | When |
|---|---|---|
WorkflowNotRegisteredException | not_found | Unknown workflow name |
WorkflowRunNotFoundException | not_found | Unknown workflow run ID |
WorkflowConflictException | conflict | Resume/state conflict |
BadWorkflowRequestException | invalid_request | Invalid input or request shape |
IllegalArgumentException | invalid_request | Invalid arguments |
Any other RuntimeException | internal_error | Unexpected failure |
Each failure returns a structured error object with error (message) and errorType fields, keeping agent clients from seeing Spring-specific or Ktor-specific exception shapes.
Transport: Stdio
Stdio transport starts an MCP session on the process's standard input/output streams. This is the mode used by local agent tools (Claude Desktop, VS Code MCP extensions, CLI agents):
tramai:
mcp:
stdio:
enabled: true
The server creates a StdioServerTransport from System.in and System.out and starts a session on the application's CoroutineScope. Useful when the same JVM process is both the workflow host and the agent's subprocess target.
Transport: SSE
SSE transport starts an embedded Ktor CIO server on a separate port. This is the mode for network-accessible MCP clients:
tramai:
mcp:
sse:
enabled: true
host: 127.0.0.1
port: 8091
path: /mcp
The SSE server is independent from the Spring Boot HTTP port. The Ktor routing block mounts the MCP server at the configured path, and the MCP Kotlin SDK's mcp(path) { ... } routing extension handles the SSE and JSON-RPC message protocol.
Relationship to REST API
Every MCP tool call ultimately flows through the same code path as a REST API request. For example:
MCP run_workflow("invoice", { "invoiceId": "abc" })
→ McpToolHandlers.runWorkflow()
→ WorkflowController.runWorkflow(name, body, idempotencyKey)
→ WorkflowEngine.run(...)
This means:
- Workflow validation is identical
- Idempotency behavior is identical
- Conflict rules are identical
- Run status semantics are identical
The adapter does not introduce a separate execution path.
L3: Architecture & Mechanics (15-minute read)
Module dependency graph
tramai-mcp
│
├── api: tramai-server
│ └── WorkflowRegistry, WorkflowRunStore, WorkflowController
│
├── api: tramai-structured
│ └── JacksonStructuredOutputHandler (schema generation)
│
├── impl: MCP Kotlin SDK (io.modelcontextprotocol:kotlin-sdk-server:0.11.1)
│ └── Server, StdioServerTransport, Tool, ToolSchema, CallToolResult
│
├── impl: Ktor Server CIO (io.ktor:ktor-server-cio-jvm:3.3.3)
│ └── EmbeddedServer, routing, mcp(path) extension
│
├── impl: Spring Boot autoconfigure
│ └── SmartLifecycle, @AutoConfiguration, @ConfigurationProperties
│
└── impl: kotlinx-serialization-json, Jackson, Okio
Class responsibilities
Three production source files, one test file.
TramaiMcpServer.kt
The core MCP server adapter. Constructs an MCP SDK Server instance with four registered tools, then provides two lifecycle methods for transport:
class TramaiMcpServer(
private val handlers: McpToolHandlers,
) : AutoCloseable
Server construction (init block): Creates an MCP SDK Server with:
serverInfo— name"tramai-mcp", version"0.3.1"capabilities— tools support (listChanged = false)- Four
addTool(...)registrations, each wiring a tool definition to its handler viahandleCall { ... }
Each tool is defined by a private Tool(...) factory method that provides:
name— the MCP tool name stringdescription— human-readable descriptioninputSchema— parsed from inline JSON viaschema()helper, which deserializes a raw JSON string into aToolSchemawithproperties,required, and optionaldefsoutputSchema— sameschema()helper pattern
Tool argument extraction: Three private extension functions on CallToolRequest:
requiredString(name)— extracts and returns a string, throwsToolExecutionExceptionwith"invalid_request"if missingoptionalString(name)— extracts a nullable string, returningnullfor absent orJsonNullvaluesrequiredObject(name)— extracts aJsonElementargument, throwsToolExecutionExceptionif missing
handleCall dispatcher: A private function wrapping every tool handler:
private fun handleCall(action: () -> JsonObject): CallToolResult = try {
val payload = action()
CallToolResult(
content = listOf(TextContent(Json.encodeToString(...))),
structuredContent = payload,
)
} catch (error: ToolExecutionException) {
CallToolResult(
content = listOf(TextContent(error.message)),
isError = true,
structuredContent = error.asResult(),
)
}
Success returns both a TextContent string and structuredContent JSON object. Failure returns the error message as TextContent and the structured { error, errorType } object.
Transport methods:
startStdio()— Launches a coroutine on the module'sCoroutineScope(IO)that creates aStdioServerTransportfromSystem.in/System.out(via OkioasSource()/asSink()), then callsmcpServer.createSession(transport). This is a one-shot session — useful when the process is spawned specifically for MCP.startSse(host, port, path)— Creates anembeddedServer(CIO)with a routing block that mounts the MCP server via the Ktormcp(path) { mcpServer }extension. Starts non-blocking (wait = false). The embedded server is stored so it can be stopped later.stopSse()— Stops the embedded SSE server with 1-second grace and timeout periods.close()— ImplementsAutoCloseable. Stops SSE, cancels the coroutine scope, and closes the MCP server session.
The sdkServer() accessor returns the raw MCP SDK Server instance, enabling programmatic session creation (used in tests for in-process stdio client testing via piped streams).
McpToolHandlers.kt
The business-logic adapter that translates MCP tool calls into tramai-server operations. Constructed with:
class McpToolHandlers(
private val registry: WorkflowRegistry,
private val runStore: WorkflowRunStore,
private val workflowController: WorkflowController,
private val objectMapper: ObjectMapper,
private val structuredOutputHandler: JacksonStructuredOutputHandler,
)
listWorkflows(): Iterates registry.list(), building a JSON array where each entry includes the workflow name, definition version, and JSON schemas for the input state type and result type. Schema generation delegates to structuredOutputHandler.generateSchema(type), which uses the same structured-output infrastructure that validates LLM responses — guaranteeing that the schema exposed via MCP matches what the workflow engine actually expects.
runWorkflow(workflowName, state, idempotencyKey): Serializes the incoming JsonElement state to a Jackson JsonNode via objectMapper.writeValueAsString(), then calls workflowController.runWorkflow(). Catches RuntimeException subtypes and translates them via toolFailure().
resumeWorkflow(workflowId, workflowName): If workflowName is provided and valid, uses it directly. Otherwise, calls resolveWorkflowName() which scans all registered workflows and queries each one's checkpoint store to find which workflow owns the given ID. This allows callers to resume runs without knowing which workflow they belong to.
getWorkflowStatus(workflowId, workflowName): Same name resolution logic as resumeWorkflow, then calls workflowController.getRun() to fetch the full WorkflowRunDetail including current step, history array, result, and error state. Returns the serialized detail as a JSON object.
resolveWorkflowName(workflowId, workflowName): Two-phase resolution:
- If
workflowNameis non-blank, validates it exists in the registry and returns it - Otherwise, iterates every registered workflow and queries
runStore.get(workflow.name, workflowId)until a match is found; throwsToolExecutionException("not_found")if no match
toolFailure(error): Pattern-matches RuntimeException subtypes to MCP error types:
WorkflowNotRegisteredException→"not_found"WorkflowRunNotFoundException→"not_found"WorkflowConflictException→"conflict"BadWorkflowRequestException/IllegalArgumentException→"invalid_request"- Everything else →
"internal_error"(with the original exception as cause)
ToolExecutionException: A custom RuntimeException subclass that carries an errorType string and produces a structured { error, errorType } JSON object via asResult(). This is the contract between the handlers and the handleCall wrapper in TramaiMcpServer.
TramaiMcpAutoConfiguration.kt
Spring Boot auto-configuration that wires the MCP adapter into the application context. Annotated with @AutoConfiguration and @ConditionalOnBean(WorkflowRegistry, WorkflowRunStore, WorkflowController).
Beans registered:
| Bean | Type | Purpose |
|---|---|---|
tramaiMcpStructuredOutputHandler | JacksonStructuredOutputHandler | Schema generation for workflow input/output types |
mcpToolHandlers | McpToolHandlers | Bridges MCP calls to server controllers |
tramaiMcpServer | TramaiMcpServer | MCP SDK server with tool registrations |
tramaiMcpLifecycle | SmartLifecycle | Auto-starts transports at application startup |
TramaiMcpProperties — Binds tramai.mcp.* configuration:
stdio.enabled— defaultfalsesse.enabled— defaultfalsesse.host— default"127.0.0.1"sse.port— default8091sse.path— default"/mcp"
TramaiMcpLifecycle — A SmartLifecycle bean that:
- In
start(): checks the property flags and calls the appropriate transport start methods on the server - In
stop(): callsserver.close()to tear down transports and coroutine scope isAutoStartup()returnstrueso transports start automatically- Only sets
running = trueif at least one transport was enabled
Handler dispatch chain
The complete flow from MCP client to workflow engine:
MCP Client
│ tools/list → Server.listTools()
│ └─ returns all 4 Tool definitions
│
│ tools/call → {
│ name: "run_workflow",
│ arguments: { workflowName: "invoice", state: { ... } }
│ }
│ └─ MCP SDK dispatches to registered Tool handler
│ └─ TramaiMcpServer.addTool(runWorkflowTool()).handleCall
│ └─ handlers.runWorkflow(workflowName, state, idempotencyKey)
│ └─ workflowController.runWorkflow(name, body, idempotencyKey)
│ └─ WorkflowEngine.run(...)
│ └─ returns WorkflowRunDetail
│
│ CallToolResult {
│ content: [TextContent(json)],
│ structuredContent: { workflowId, status, ... }
│ }
Schema generation
Tool schemas are generated from Kotlin types using JacksonStructuredOutputHandler.generateSchema(type), the same component that generates JSON Schema for structured LLM output parsing. This guarantees consistency between:
- What the MCP client sees as the workflow's input/output contract
- What the workflow engine actually deserializes at runtime
- What the REST API documentation describes
The schema() helper in TramaiMcpServer parses a raw JSON string into the MCP SDK's ToolSchema type, splitting properties, required, and $defs from the parsed object:
private fun schema(raw: String): ToolSchema {
val schema = Json.parseToJsonElement(raw).jsonObject
val required = schema["required"]?.jsonArray?.map { it.jsonPrimitive.content }.orEmpty()
return ToolSchema(
properties = schema["properties"]?.jsonObject ?: buildJsonObject {},
required = required,
defs = schema["\$defs"]?.jsonObject,
)
}
Design decisions
- Adapter, not runtime — The module is deliberately thin. All execution logic lives in
tramai-serverandtramai-orchestration. The MCP adapter is only responsible for protocol translation. - Two transports, one server — The same
TramaiMcpServerinstance can serve both stdio and SSE simultaneously. TheSmartLifecyclechecks each flag independently and starts the relevant transports. - Explicit tool schemas — Each tool's input and output schemas are defined as inline JSON strings in
TramaiMcpServer.kt, not auto-generated from Kotlin types at runtime. This keeps the tool definitions predictable and easy to audit. The schemas for workflow-specific state types (input/output) are auto-generated viaJacksonStructuredOutputHandleratlistWorkflowstime. - Workflow name resolution — Both
resume_workflowandget_workflow_statusaccept an optionalworkflowName. When omitted, the handler scans all registered workflows to find the one that owns the given run ID. This simplifies the client contract — an agent can stash a workflow ID and call status/resume without tracking which workflow it belongs to. - Structured error payloads — Error responses include both a human-readable
TextContentmessage and a structured JSON payload witherroranderrorTypefields, enabling both agent-facing error messages and programmatic error classification.
Current limits
- No
cancellationtool — workflow runs cannot be cancelled via MCP (use the REST API) - No
tools/notifications—listChangedisfalse; clients must re-list to discover new workflows - Single MCP session — the stdio transport creates exactly one session; for multi-connection scenarios, use SSE
- SSE server is a separate Ktor instance — does not reuse the Spring Boot HTTP port or its TLS configuration
