From morpheus
Use this skill when the user wants to visualize, build, or share a Morpho lending strategy as a node graph. Morpheus is a visual canvas editor for Morpho strategies. Given a strategy goal in natural language, use the Morpho MCP to discover markets and vaults, then call the Morpheus API to generate a deep-link URL the user can open in their browser to see and execute the strategy. Trigger phrases: "visualize this strategy in Morpheus", "build a Morpho strategy and show me", "give me a Morpheus link for X", "create a yield strategy on Base/Ethereum", "open this in Morpheus".
npx claudepluginhub skar8848/morpheus --plugin morpheusThis skill uses the workspace's default tool permissions.
Morpheus is a visual node-graph editor for composing Morpho lending strategies. It exposes a stateless HTTP API so any agent can build a strategy server-side and hand the user a deep-link URL that opens directly into a pre-built canvas.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Morpheus is a visual node-graph editor for composing Morpho lending strategies. It exposes a stateless HTTP API so any agent can build a strategy server-side and hand the user a deep-link URL that opens directly into a pre-built canvas.
A canvas is a directed graph of nodes connected by edges:
vaultDeposit (direct earn), supplyCollateral (borrow flow), swap, repayborrow — never as a "transfer" bridge. Connects from wallet/swap/vaultWithdraw/position. Connects to: borrowsupplyCollateral. Connects to: swap, vaultDepositwallet/borrow/vaultWithdraw. Connects to: vaultDeposit/supplyCollateral/wallet/repaywallet (pure earn), borrow (carry trade), swap, or vaultWithdraw (rebalance).position. Connects to: swap/vaultDeposit/supplyCollateral/repay| User intent | Correct flow |
|---|---|
| "Deposit X into vault Y" (pure earn) | wallet → vaultDeposit (2 nodes) |
| "Supply X as collateral and borrow Y" | wallet → supplyCollateral → borrow |
| "Borrow against my collateral and farm the borrowed asset in a vault" (carry trade) | wallet → supplyCollateral → borrow → vaultDeposit |
| "Looped staking" (leverage) | wallet → supplyCollateral → borrow → swap → supplyCollateral |
| "I want EURCV exposure but the Morpho USDC/EURCV market has no liquidity" | wallet → swap (USDC→EURCV via CowSwap) → vaultDeposit |
Do NOT use supplyCollateral as a bridge for direct vault deposits — it has different semantics (it touches a Morpho Blue market, not the vault's underlying asset). The connection rules now allow wallet → vaultDeposit directly.
repayWhen withdrawCollateralAfterRepay is set, the repay node becomes a SOURCE
of the freed collateral asset. You can connect it directly to a swap,
supplyCollateral, or vaultDeposit node downstream — DO NOT insert a fake
wallet node in between.
✅ Correct (4 nodes, 3 edges):
vaultWithdraw → repay (free WBTC) → swap (WBTC → USDC) → vaultDeposit (USDC vault)
❌ Wrong (5 nodes, broken — the wallet doesn't know about the future WBTC):
vaultWithdraw → repay (free WBTC)
(no edge)
wallet → swap (WBTC → USDC) ← swap can't auto-detect WBTC, has to be picked manually
The downstream swap / supplyCollateral / vaultDeposit nodes auto-detect
the freed asset and amount from the upstream repay (reading
market.collateralAsset + collateralToWithdraw). Just wire the edge
directly.
VALID_CONNECTIONS includes repay → ["swap", "supplyCollateral", "vaultDeposit"]
when withdrawCollateralAfterRepay is true.
The repay node has two extra optional fields that turn it into a "close out
this borrow" primitive:
{
"id": "repay-1",
"type": "repayNode",
"data": {
"type": "repay",
"market": { /* full Market object — same as borrow node */ },
"amount": "1300.08", // human units
"amountUsd": 1300.08,
// OPTIONAL — free the collateral after the repay
"withdrawCollateralAfterRepay": true,
"collateralToWithdraw": "920000000000000000" // raw units (wei) of the user's full collateral
}
}
When withdrawCollateralAfterRepay is true and collateralToWithdraw is set,
the executor emits an extra morpho.withdrawCollateral call AFTER the repay,
sending the freed collateral straight to the user's wallet. The bundle becomes:
erc20TransferFrom (loan token from user → adapter)morphoRepay (repays the debt)morphoWithdrawCollateral (frees the collateral → user wallet)When to set it: any time the user says things like "close my borrow",
"repay and free my wstETH", "unwind this position", or when you're doing
a full repay (amount ≈ position's borrow). Always read the user's current
collateral from /api/positions and pass the raw value (the collateral
field on the market position object, already in token units).
Why it matters: without this, the user has to manually call
morpho.withdrawCollateral outside Morpheus to get their collateral back.
Adding it to the canvas makes the strategy self-contained.
The Morpho MCP only knows about Morpho markets. It does NOT know that Morpheus also supports CowSwap (intent-based swaps, Ethereum mainnet only). When you query a market via morpho_query_markets and find that the total liquidity is too low to satisfy the user's intent, do not give up — fall back to a swap node.
Detection: a Morpho market is "too thin" when state.liquidityAssets / 10^decimals is less than the user's borrow amount (with a 10% safety buffer). Common cases on Ethereum mainnet: USDC↔EURCV, niche stablecoins, freshly-launched markets.
Pattern: replace the broken borrow branch with a CowSwap swap from a liquid asset the user holds (or can get cheaply) to the desired token. Example:
wallet → supplyCollateral(USDC) → borrow(EURCV) — fails because USDC/EURCV market has $73 of liquiditywallet → swap(USDC → EURCV) — uses CowSwap's cross-DEX liquidity directlyThe fallback is:
Hard rule: NEVER use swap on a Base canvas. The CowSwap Vault Relayer is only deployed on Ethereum (chainId 1). If the chain is base, find an alternative path or tell the user.
When mentioning CowSwap to the user, frame it as: "The Morpho USDC/EURCV market only has $X of liquidity, so I'm routing through CowSwap instead — same end state, no borrow involved."
POST /api/canvas — production URL: https://morpheus-visualizer.vercel.app. Override with the MORPHEUS_BASE_URL env var only when targeting a self-hosted instance or http://localhost:3000 for local dev. Always default to production.
{
"chain": "ethereum" | "base",
"sourceAddress": "0x...",
"nodes": [
{
"id": "string", // unique within request
"type": "walletNode" | "supplyCollateralNode" | "borrowNode" | "swapNode" | "vaultDepositNode" | "vaultWithdrawNode" | "repayNode" | "positionNode",
"position": { "x": number, "y": number },
"data": { "type": "wallet" | "supplyCollateral" | ... , /* node-specific */ }
}
],
"edges": [
{
"id": "string",
"source": "<node id>",
"target": "<node id>",
"type": "animatedEdge",
"animated": true
}
]
}
Returns:
{
"ok": true,
"deepLinkUrl": "http://localhost:3000/ethereum/canvas?strategy=eyJjaGFp...",
"chain": "ethereum",
"nodeCount": 4,
"edgeCount": 3
}
Validation errors come back as { ok: false, errors: [...] }. Limits: 100 KB body, 200 nodes, 500 edges. CORS is *.
The standard agent flow combines two tools:
morpho_query_vaults, morpho_query_markets, morpho_get_vault, etc.) → discover the best vaults/markets for the user's goal. The MCP returns full data including addresses, decimals, APYs./api/canvas → wrap that data into a node graph and get a shareable URL.Do NOT use morpho_get_positions for vault position discovery. The Morpho MCP's morpho_get_positions tool only returns V1 (legacy MetaMorpho) vault positions — it silently drops V2 vault positions. Most new vaults launched on Ethereum and Base in 2025+ are V2 (Steakhouse Prime Instant, Gauntlet's newer vaults, etc.). If the user mentions a vault and morpho_get_positions returns "no position found", it is almost certainly a V2 vault that the legacy query doesn't see.
Always use the Morpheus /api/positions endpoint instead. It returns BOTH V1 and V2 positions in a single flat response:
GET https://morpheus-visualizer.vercel.app/api/positions?address=0xUSER&chain=ethereum
Response (flat, agent-friendly):
{
"ok": true,
"address": "0x...",
"chain": "ethereum",
"chainId": 1,
"marketPositions": [
{
"marketId": "0x...",
"collateralSymbol": "wstETH",
"collateralAddress": "0x...",
"collateralDecimals": 18,
"loanSymbol": "EURCV",
"loanAddress": "0x...",
"loanDecimals": 18,
"collateral": "920000000000000000",
"collateralUsd": 3500.42,
"borrow": "1300080000000000000000",
"borrowUsd": 1300.08,
"healthFactor": 1.85,
"lltv": "860000000000000000"
}
],
"vaultPositions": [
{
"vaultAddress": "0x...",
"vaultName": "Steakhouse Prime Instant EURCV",
"vaultSymbol": "stkEURCV",
"assetSymbol": "EURCV",
"assetAddress": "0x...",
"assetDecimals": 18,
"shares": "...",
"assets": "...",
"assetsUsd": 1200.50,
"netApy": 0.0574,
"version": "v2"
}
],
"counts": { "markets": 2, "vaultsV1": 0, "vaultsV2": 1 }
}
For market discovery (morpho_query_markets) and vault discovery (morpho_query_vaults) the Morpho MCP is fine — those queries don't have the V2 hole. The bug is only on morpho_get_positions for vault holdings.
morpho_query_vaults / morpho_query_markets to find candidates on the right chainwalletNode (no incoming edges)animatedEdge edgesdata.asset.address, decimals from the API, etc./api/canvasdeepLinkUrl to the user with a one-line description of what they'll seeThis is a pure earn flow — wallet → vaultDeposit directly. NO supplyCollateral node.
// Step 1: discover the best vault
const vaults = await callMcpTool("morpho_query_vaults", {
chain: "base",
asset_symbol: "USDC",
sort: "apy_desc",
limit: 1,
});
const best = vaults.items[0];
// Step 2: build a 2-node canvas — wallet directly to vault
const canvas = {
chain: "base",
sourceAddress: userAddress,
nodes: [
{
id: "w1",
type: "walletNode",
position: { x: 50, y: 200 },
data: {
type: "wallet",
address: userAddress,
chain: "base",
chainId: 8453,
balances: [],
},
},
{
id: "v1",
type: "vaultDepositNode",
position: { x: 380, y: 200 },
data: {
type: "vaultDeposit",
vault: {
address: best.address,
name: best.name,
symbol: best.symbol,
asset: {
symbol: "USDC",
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
decimals: 6,
logoURI: "https://cdn.morpho.org/assets/logos/usdc.svg",
},
state: {
totalAssets: "0",
totalAssetsUsd: best.tvlUsd,
curator: null,
netApy: best.apyPct / 100,
fee: 0,
allocation: [],
},
},
amount: "10000",
amountUsd: 10000,
depositAll: false,
},
},
],
edges: [
{ id: "e1", source: "w1", target: "v1", type: "animatedEdge", animated: true },
],
};
// Step 3: POST
const res = await fetch(`https://morpheus-visualizer.vercel.app/api/canvas`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(canvas),
});
const { deepLinkUrl } = await res.json();
// Step 4: present to user
return `Here's your Morpheus deep link: ${deepLinkUrl}\n\nClick to open the canvas with the strategy pre-built. Review the simulation, then click Execute to sign.`;
This is a carry trade — supplyCollateral IS appropriate here because you're posting wstETH on a Morpho Blue market to borrow against it.
wallet → supplyCollateral(wstETH) → borrow(wstETH/USDC market) → vaultDeposit(USDC vault)
4 nodes, 3 edges, in that exact order.
undefined.chain: "ethereum" (chainId 1) or chain: "base" (chainId 8453) — match the chain of the markets/vaults you found./api/canvas/validate first to check the schema without generating a URL.swapNode for Base canvases.vaultDeposit or repay nodes.Don't ask repeatedly. If the user is testing locally, leave sourceAddress as "0x0000000000000000000000000000000000000000" and let them connect their wallet inside Morpheus. The wallet node will be re-injected with their actual address when the canvas loads.
The user can also load 4 pre-built templates from Morpheus's sidebar without an agent:
If the user's goal matches one of these, suggest the template name first — it's faster than building manually.
After POSTing successfully, mention what the user will see when they open the link:
The deep-link URL is always huge because the entire canvas JSON is encoded
in the ?strategy= param. Pasting it raw makes the chat ugly and unreadable.
ALWAYS format the URL as a Markdown link so the chat renders a clean clickable label. NEVER paste the full URL on its own line.
✅ Good (short markdown link):
Here's your strategy:
2 nodes — Wallet → Gauntlet USDC Core, ready to deposit 5,000 USDC. Click to open, connect your wallet, and review before signing.
❌ Bad (raw URL):
Here's your link:
The Markdown [label](url) syntax is rendered by Claude Code as a clickable
link, hiding the giant query string. Use a clear action label like:
→ Open in MorpheusView strategy in MorpheusVisualize on MorpheusIf you absolutely need to show the URL fragment for debugging, truncate to the
first ~30 chars + ….