Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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`.)
Expand All @@ -69,16 +70,20 @@ Run any of them via `exec:java`:
<dependency>
<groupId>finance.idem</groupId>
<artifactId>idem-sdk-kotlin</artifactId>
<version>0.0.11-test</version>
<version>0.0.12-test</version>
</dependency>
```

`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)
Expand Down
Empty file modified mvnw
100644 → 100755
Empty file.
13 changes: 11 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,16 @@
<java.version>21</java.version>
<kotlin.version>1.9.25</kotlin.version>
<!--
finance.idem:idem-sdk-kotlin has not had a stable release yet — 0.0.11-test
finance.idem:idem-sdk-kotlin has not 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. Update this once a real
0.x/1.x release ships (see github.com/idem-finance/idem, issue #233).
-->
<idem-sdk.version>0.0.11-test</idem-sdk.version>
<idem-sdk.version>0.0.12-test</idem-sdk.version>
<!-- Pinned to the same io.modelcontextprotocol.sdk:mcp version pulled in by
the main idem repo's mcp module (via spring-ai-bom 1.0.0), to guarantee
wire compatibility with IdemMcpServer's SSE transport. -->
<mcp-sdk.version>0.10.0</mcp-sdk.version>
</properties>

<dependencies>
Expand All @@ -44,6 +48,11 @@
<artifactId>idem-sdk-kotlin</artifactId>
<version>${idem-sdk.version}</version>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>${mcp-sdk.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
Expand Down
160 changes: 160 additions & 0 deletions src/main/kotlin/finance/idem/examples/mcp/McpAgentWorkflowExample.kt
Original file line number Diff line number Diff line change
@@ -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)
}

This file was deleted.

Loading