diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..1ad374d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,28 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v5
+ with:
+ java-version: '21'
+ distribution: temurin
+ cache: maven
+
+ - name: Build and test
+ run: ./mvnw verify --no-transfer-progress
diff --git a/README.md b/README.md
index 0b3036a..3e108ba 100644
--- a/README.md
+++ b/README.md
@@ -41,14 +41,13 @@ docker compose run --rm -e SPRING_PROFILES_ACTIVE=dev,seed app
|---|---|---|
| 01 | [`basic/BasicTransactionExample.kt`](src/main/kotlin/finance/idem/examples/basic/BasicTransactionExample.kt) | A simple fiat double-entry transaction — debit/credit, `postTransaction`, `getBalance` |
| 02 | [`onchain/StablecoinOnChainExample.kt`](src/main/kotlin/finance/idem/examples/onchain/StablecoinOnChainExample.kt) | A cross-border stablecoin transaction mixing on-chain entries in one transaction, with an explicit idempotency key |
-| 03 | [`settlement/PendingSettlementReadme.md`](src/main/kotlin/finance/idem/examples/settlement/PendingSettlementReadme.md) | Conceptual doc — how server-side settlement actually works today (chain-reader/webhook-driven); not yet exposed via the SDK, so there's no runnable code here |
-| 04 | [`reconciliation/ReconciliationExample.kt`](src/main/kotlin/finance/idem/examples/reconciliation/ReconciliationExample.kt) | Posting via the SDK, then reconciling via a direct REST call (no SDK method for this yet) |
-| 05 | [`mcp/McpAgentWorkflowReadme.md`](src/main/kotlin/finance/idem/examples/mcp/McpAgentWorkflowReadme.md) | Connecting Claude Code to Idem's MCP server and driving the ledger with natural-language prompts instead of code |
+| 03 | [`settlement/PendingSettlementExample.kt`](src/main/kotlin/finance/idem/examples/settlement/PendingSettlementExample.kt) | The settlement lifecycle — `registerSettlement`, forcing a match via `reconcileBatch`, `getSettlement` showing `SETTLED`, and `cancelSettlement` showing `CANCELLED` |
+| 04 | [`reconciliation/ReconciliationExample.kt`](src/main/kotlin/finance/idem/examples/reconciliation/ReconciliationExample.kt) | Posting via the SDK, then reconciling via `IdemClient.reconcileBatch` |
+| 05 | [`mcp/McpAgentWorkflowExample.kt`](src/main/kotlin/finance/idem/examples/mcp/McpAgentWorkflowExample.kt) | A real MCP client (official `io.modelcontextprotocol.sdk:mcp`) driving `postTransaction` -> `reconcileBatch` -> `rollbackWorkflow` -> `getAgentAuditLog` over SSE — the same tools you can also drive via natural-language prompts in Claude Code/Desktop |
-Every code example creates its own accounts on first run — `idem-sdk-kotlin`
-doesn't expose account creation, so each one bootstraps what it needs via
-`support/ExampleAccounts.kt`, a small helper shared across examples 01, 02,
-and 04.
+Every code example creates its own accounts on first run via
+`support/ExampleAccounts.kt`, a small helper around `IdemClient.createAccount`
+shared across examples 01, 02, 03, 04, and 05.
## Running a specific example
@@ -58,7 +57,9 @@ Run any of them via `exec:java`:
```bash
./mvnw compile exec:java -Dexec.mainClass=finance.idem.examples.basic.BasicTransactionExampleKt
./mvnw compile exec:java -Dexec.mainClass=finance.idem.examples.onchain.StablecoinOnChainExampleKt
+./mvnw compile exec:java -Dexec.mainClass=finance.idem.examples.settlement.PendingSettlementExampleKt
./mvnw compile exec:java -Dexec.mainClass=finance.idem.examples.reconciliation.ReconciliationExampleKt
+./mvnw compile exec:java -Dexec.mainClass=finance.idem.examples.mcp.McpAgentWorkflowExampleKt
```
(Kotlin compiles a top-level `main()` in `Foo.kt` to a class named `FooKt`.)
@@ -69,16 +70,20 @@ Run any of them via `exec:java`:
finance.idem
idem-sdk-kotlin
- 0.0.11-test
+ 0.0.12-test
```
-`idem-sdk-kotlin` hasn't had a stable release yet — `0.0.11-test` is the
+`idem-sdk-kotlin` hasn't had a stable release yet — `0.0.12-test` is the
latest pre-release build published to Maven Central while the `idem` release
pipeline is under active development
([idem-finance/idem#233](https://github.com/idem-finance/idem/issues/233)).
Update this version once a real `0.x`/`1.x` release ships.
+Example 05 (`mcp/McpAgentWorkflowExample.kt`) also depends on the official
+`io.modelcontextprotocol.sdk:mcp` client, pinned to the same version the main
+repo's MCP server pulls in via `spring-ai-bom`.
+
## Links
- Main repo: [github.com/idem-finance/idem](https://github.com/idem-finance/idem)
diff --git a/mvnw b/mvnw
old mode 100644
new mode 100755
diff --git a/pom.xml b/pom.xml
index 8cc7b96..5e7a0c9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,12 +30,16 @@
21
1.9.25
- 0.0.11-test
+ 0.0.12-test
+
+ 0.10.0
@@ -44,6 +48,11 @@
idem-sdk-kotlin
${idem-sdk.version}
+
+ io.modelcontextprotocol.sdk
+ mcp
+ ${mcp-sdk.version}
+
org.springframework.boot
spring-boot-starter
diff --git a/src/main/kotlin/finance/idem/examples/mcp/McpAgentWorkflowExample.kt b/src/main/kotlin/finance/idem/examples/mcp/McpAgentWorkflowExample.kt
new file mode 100644
index 0000000..86c8e6a
--- /dev/null
+++ b/src/main/kotlin/finance/idem/examples/mcp/McpAgentWorkflowExample.kt
@@ -0,0 +1,160 @@
+package finance.idem.examples.mcp
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import finance.idem.examples.support.allowAgentMaxDebitPerSession
+import finance.idem.examples.support.createAccount
+import finance.idem.examples.support.mintAgentApiKey
+import finance.idem.sdk.IdemClient
+import io.modelcontextprotocol.client.McpClient
+import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport
+import io.modelcontextprotocol.spec.McpSchema
+import kotlinx.coroutines.runBlocking
+import java.time.Duration
+import java.time.Instant
+import java.time.temporal.ChronoUnit
+import java.util.UUID
+
+/**
+ * Example 05 — a real MCP client driving Idem's agent tools, mirroring the
+ * "demo scenario" covered by the main repo's `McpServerIntegrationTest`:
+ * post -> reconcile -> rollback -> audit log.
+ *
+ * Unlike every other example here, this one does NOT go through
+ * `idem-sdk-kotlin` for the ledger operations — the MCP server
+ * (`IdemMcpServer` in the main repo's `mcp` module) is a separate protocol
+ * surface (SSE/JSON-RPC), reached with the official MCP Java SDK
+ * (`io.modelcontextprotocol.sdk:mcp`) instead of an HTTP client. `IdemClient`
+ * is only used here for one-time setup: bootstrapping accounts and minting
+ * the agent-scoped API key.
+ *
+ * Every `postTransaction` call is evaluated by `PolicyGuard` before it
+ * commits, and the default policy is deny-all — so this example configures a
+ * permissive `MAX_DEBIT_PER_SESSION` rule for the minted agent key first.
+ *
+ * You can drive the exact same 4 tools through natural-language prompts in
+ * Claude Code/Desktop instead of this Kotlin client — see the connection
+ * instructions in the main repo's `docs/mcp-server.md`.
+ *
+ * Run with:
+ * ./mvnw compile exec:java -Dexec.mainClass=finance.idem.examples.mcp.McpAgentWorkflowExampleKt
+ */
+fun main() =
+ runBlocking {
+ val baseUrl = System.getenv("IDEM_BASE_URL") ?: error("IDEM_BASE_URL is not set — see .env.example")
+ val apiKey = System.getenv("IDEM_API_KEY") ?: error("IDEM_API_KEY is not set — see .env.example")
+ val objectMapper = ObjectMapper()
+
+ val client = IdemClient(baseUrl = baseUrl, apiKey = apiKey)
+ val (fiatAccountId, usdcAccountId, agentApiKey) =
+ client.use {
+ val fiatAccountId = client.createAccount(name = "MCP Agent Fiat", currency = "USD", type = "ASSET")
+ val usdcAccountId = client.createAccount(name = "MCP Agent USDC", currency = "USD", type = "ASSET")
+
+ val agentApiKey = client.mintAgentApiKey(listOf("AGENTS_EXECUTE", "AGENTS_ROLLBACK", "AGENTS_AUDIT_READ"))
+ println("Minted agent API key with prefix ${agentApiKey.prefix}")
+
+ client.allowAgentMaxDebitPerSession(agentApiKey.prefix, amount = "100000.00")
+ println("Configured a permissive MAX_DEBIT_PER_SESSION policy rule for ${agentApiKey.prefix}")
+
+ Triple(fiatAccountId, usdcAccountId, agentApiKey)
+ }
+
+ val sessionId = UUID.randomUUID().toString()
+ val agentId = "idem-examples-mcp-demo"
+
+ val transport =
+ HttpClientSseClientTransport
+ .builder(baseUrl)
+ .sseEndpoint("/sse")
+ .customizeRequest { it.header("X-API-Key", agentApiKey.rawKey) }
+ .build()
+
+ val mcpClient = McpClient.sync(transport).requestTimeout(Duration.ofSeconds(30)).build()
+ try {
+ mcpClient.initialize()
+ println("Connected to Idem MCP server — ${mcpClient.listTools().tools().size} tools available")
+
+ val postResult =
+ mcpClient.callTool(
+ McpSchema.CallToolRequest(
+ "postTransaction",
+ mapOf(
+ "entries" to
+ listOf(
+ mapOf(
+ "accountId" to fiatAccountId.toString(),
+ "entryType" to "DEBIT",
+ "monetaryEntryType" to "FIAT",
+ "amount" to "300.00",
+ "currency" to "USD",
+ "rail" to "WIRE",
+ ),
+ mapOf(
+ "accountId" to usdcAccountId.toString(),
+ "entryType" to "CREDIT",
+ "monetaryEntryType" to "FIAT",
+ "amount" to "300.00",
+ "currency" to "USD",
+ "rail" to "WIRE",
+ ),
+ ),
+ "idempotencyKey" to UUID.randomUUID().toString(),
+ "intentDescription" to "idem-examples MCP agent workflow demo",
+ "agentId" to agentId,
+ "sessionId" to sessionId,
+ ),
+ ),
+ )
+ val workflowPlanId = printToolResult("postTransaction", postResult, objectMapper).get("workflowPlanId").asText()
+
+ val reconcileResult =
+ mcpClient.callTool(
+ McpSchema.CallToolRequest(
+ "reconcileBatch",
+ mapOf(
+ "accountId" to usdcAccountId.toString(),
+ "from" to Instant.now().minus(1, ChronoUnit.DAYS).toString(),
+ "to" to Instant.now().toString(),
+ ),
+ ),
+ )
+ printToolResult("reconcileBatch", reconcileResult, objectMapper)
+
+ val rollbackResult =
+ mcpClient.callTool(
+ McpSchema.CallToolRequest(
+ "rollbackWorkflow",
+ mapOf(
+ "workflowPlanId" to workflowPlanId,
+ "reason" to "idem-examples MCP agent workflow demo — compensating the demo transaction",
+ "agentId" to agentId,
+ "sessionId" to sessionId,
+ ),
+ ),
+ )
+ printToolResult("rollbackWorkflow", rollbackResult, objectMapper)
+
+ val auditResult =
+ mcpClient.callTool(
+ McpSchema.CallToolRequest(
+ "getAgentAuditLog",
+ mapOf("sessionId" to sessionId, "limit" to 10),
+ ),
+ )
+ val auditJson = printToolResult("getAgentAuditLog", auditResult, objectMapper)
+ println("Audit trail has ${auditJson.get("total").asInt()} event(s)")
+ } finally {
+ mcpClient.closeGracefully()
+ }
+ }
+
+private fun printToolResult(
+ toolName: String,
+ result: McpSchema.CallToolResult,
+ objectMapper: ObjectMapper,
+): com.fasterxml.jackson.databind.JsonNode {
+ val text = (result.content().first() as McpSchema.TextContent).text()
+ check(result.isError != true) { "$toolName returned an error: $text" }
+ println("$toolName -> $text")
+ return objectMapper.readTree(text)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/finance/idem/examples/mcp/McpAgentWorkflowReadme.md b/src/main/kotlin/finance/idem/examples/mcp/McpAgentWorkflowReadme.md
deleted file mode 100644
index 32f1e2f..0000000
--- a/src/main/kotlin/finance/idem/examples/mcp/McpAgentWorkflowReadme.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# Example 05 — MCP agent workflow (conceptual, not runnable Kotlin)
-
-This is a setup guide + prompt reference, not code — the point is to drive
-Idem through Claude Code (or any MCP client) directly, not through
-`idem-sdk-kotlin`. It documents the MCP server exposed by the main `idem`
-repo's `mcp` module (`IdemMcpServer.kt`).
-
-## The 7 tools, as actually registered
-
-Spring AI's `@Tool` registers MCP tools under their **Kotlin method name**,
-which is camelCase — not the snake_case (`post_transaction`, etc.) used in
-some of the main repo's older docs. Use the names below; they're read directly
-from `mcp/src/main/kotlin/finance/idem/mcp/IdemMcpServer.kt`.
-
-| Tool (real, registered name) | Required scope | Key parameters |
-|---|---|---|
-| `postTransaction` | `AGENTS_EXECUTE` | `entries` (journal lines), `idempotencyKey`, `intentDescription`, `agentId`, `sessionId` |
-| `getBalance` | `AGENTS_EXECUTE` | `accountId`, `asOf?` |
-| `listEntries` | `AGENTS_EXECUTE` | `accountId`, `from?`, `to?`, `limit?`, `cursor?` |
-| `describeAccount` | `AGENTS_EXECUTE` | `accountId` |
-| `rollbackWorkflow` | `AGENTS_ROLLBACK` | `workflowPlanId`, `reason`, `agentId`, `sessionId` |
-| `reconcileBatch` | `AGENTS_EXECUTE` | `accountId?`, `from`, `to`, `tolerancePercent?` |
-| `getAgentAuditLog` | `AGENTS_AUDIT_READ` | `sessionId?`, `from?`, `to?`, `limit?` |
-
-`AGENTS_ROLLBACK` is intentionally a separate scope from `AGENTS_EXECUTE` — an
-agent key that can post transactions cannot roll them back unless explicitly
-granted this too.
-
-Every `postTransaction` call is evaluated by `PolicyGuard` **before** it
-commits — by default, a tenant with no configured policy rules gets a
-deny-all rule (`MaxDebitPerSession(ZERO)`), so agent debits will be rejected
-until you configure a permissive rule for your dev tenant.
-
-## Connecting Claude Code
-
-The server speaks the SSE transport (`GET /sse` + `POST /mcp/messages`,
-Spring AI's `WebMvcSseServerTransportProvider`) — use `--transport sse`, not
-`http`. If you're running the stack locally per this repo's README, expose it
-first (e.g. `ngrok http 8081`), then:
-
-```bash
-claude mcp add --transport sse idem https://.ngrok.io/sse \
- --header "X-API-Key: "
-```
-
-Verify with `claude mcp list` or `/mcp` inside a session — confirm `idem`
-shows connected and all 7 tools are visible.
-
-Use an agent-scoped key (`sk_agent_...`) with only the scopes you need for the
-demo, e.g. `AGENTS_EXECUTE` + `AGENTS_ROLLBACK` + `AGENTS_AUDIT_READ` — not the
-ADMIN dev-seed key from the rest of this repo's examples.
-
-## Example prompts
-
-Once connected, natural-language prompts to Claude Code exercise the tools
-directly — no code required. A demo flow mirroring the one covered by the
-main repo's `McpServerIntegrationTest` "demo scenario" test:
-
-1. *"Post a transaction debiting account `` and crediting
- `` for 300 USD over WIRE, with idempotency key
- `demo-exec-001`."* → calls `postTransaction`, returns a `workflowPlanId`.
-2. *"Reconcile account `` for the last 24 hours."* → calls
- `reconcileBatch`, returns `matched`/`unmatched`/`exceptions`.
-3. *"Roll back workflow `` — reason: compliance
- review."* → calls `rollbackWorkflow`, returns the compensating
- transaction(s) and a `ROLLED_BACK` status.
-4. *"Show me the agent audit log for this session."* → calls
- `getAgentAuditLog`, returns HMAC-signed events — note the `PENDING`
- event written *before* execution and the `COMPLETED`/`FAILED` event
- written after, per Idem's audit-before-execution rule.
-
-## Further reading
-
-- `docs/mcp-server.md` in the main `idem` repo for the full connection guide.
-- `mcp/src/test/kotlin/finance/idem/mcp/McpServerIntegrationTest.kt` for the
- canonical end-to-end scenario this doc mirrors.
diff --git a/src/main/kotlin/finance/idem/examples/reconciliation/ReconciliationExample.kt b/src/main/kotlin/finance/idem/examples/reconciliation/ReconciliationExample.kt
index 317db9a..3528ea0 100644
--- a/src/main/kotlin/finance/idem/examples/reconciliation/ReconciliationExample.kt
+++ b/src/main/kotlin/finance/idem/examples/reconciliation/ReconciliationExample.kt
@@ -8,30 +8,19 @@ import finance.idem.sdk.model.JournalLineRequest
import finance.idem.sdk.model.OnChainEntryRequest
import finance.idem.sdk.model.PostTransactionRequest
import finance.idem.sdk.model.StablecoinToken
-import io.ktor.client.call.body
-import io.ktor.client.request.header
-import io.ktor.client.request.post
-import io.ktor.client.request.setBody
-import io.ktor.http.ContentType
-import io.ktor.http.contentType
import kotlinx.coroutines.runBlocking
import java.math.BigDecimal
import java.util.UUID
/**
- * Example 04 — reconciliation, called directly against the REST API.
+ * Example 04 — reconciliation via the real SDK method.
*
- * `IdemClient`'s entire public surface is postTransaction/getBalance/
- * listEntries/getStatement — there is no `reconcileEntries()` or
- * `rollbackWorkflow()` on the SDK today. Reconciliation IS real and reachable
- * at `POST /api/v1/reconciliation/batch` (requires RECONCILIATION_WRITE), so
- * this example posts through the SDK as usual and then reconciles with a
- * plain call through the SDK client's underlying HTTP client — the same
- * pattern used for account creation in `support/ExampleAccounts.kt`.
+ * `IdemClient.reconcileBatch` wraps `POST /api/v1/reconciliation/batch`
+ * (requires RECONCILIATION_WRITE) directly — no raw HTTP workaround needed.
*
* Rollback has NO REST or SDK path at all — it's exposed exclusively as the
* MCP tool `rollbackWorkflow` (AGENTS_ROLLBACK scope). See
- * `mcp/McpAgentWorkflowReadme.md` for how to trigger it from Claude Code.
+ * `mcp/McpAgentWorkflowExample.kt` for how to trigger it as a real MCP client.
*
* Run with:
* ./mvnw compile exec:java -Dexec.mainClass=finance.idem.examples.reconciliation.ReconciliationExampleKt
@@ -88,21 +77,15 @@ fun main() =
)
println("Posted on-chain transaction with no PENDING match: ${unmatchedTx.transactionId}")
- val results: List